auth flow
This commit is contained in:
parent
086ab9416a
commit
6c28c51373
@ -1,6 +1,8 @@
|
|||||||
import NextAuth, { NextAuthOptions } from "next-auth";
|
import NextAuth, { NextAuthOptions } from "next-auth";
|
||||||
|
import { JWT } from "next-auth/jwt";
|
||||||
import KeycloakProvider from "next-auth/providers/keycloak";
|
import KeycloakProvider from "next-auth/providers/keycloak";
|
||||||
import { jwtDecode } from "jwt-decode";
|
import { jwtDecode } from "jwt-decode";
|
||||||
|
import { DEFAULT_AUTH_COOKIE_OPTIONS } from "@/lib/cookies";
|
||||||
|
|
||||||
interface KeycloakProfile {
|
interface KeycloakProfile {
|
||||||
sub: string;
|
sub: string;
|
||||||
@ -63,23 +65,30 @@ function getRequiredEnvVar(name: string): string {
|
|||||||
|
|
||||||
async function refreshAccessToken(token: JWT) {
|
async function refreshAccessToken(token: JWT) {
|
||||||
try {
|
try {
|
||||||
|
console.log('Refreshing access token...');
|
||||||
|
|
||||||
const response = await fetch(`${process.env.KEYCLOAK_ISSUER}/protocol/openid-connect/token`, {
|
const response = await fetch(`${process.env.KEYCLOAK_ISSUER}/protocol/openid-connect/token`, {
|
||||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
},
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
client_id: process.env.KEYCLOAK_CLIENT_ID!,
|
client_id: process.env.KEYCLOAK_CLIENT_ID!,
|
||||||
client_secret: process.env.KEYCLOAK_CLIENT_SECRET!,
|
client_secret: process.env.KEYCLOAK_CLIENT_SECRET!,
|
||||||
grant_type: "refresh_token",
|
grant_type: "refresh_token",
|
||||||
refresh_token: token.refreshToken,
|
refresh_token: token.refreshToken!,
|
||||||
}),
|
}),
|
||||||
method: "POST",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const refreshedTokens = await response.json();
|
const refreshedTokens = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
console.error('Token refresh failed:', refreshedTokens);
|
||||||
throw refreshedTokens;
|
throw refreshedTokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Token refreshed successfully');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...token,
|
...token,
|
||||||
accessToken: refreshedTokens.access_token,
|
accessToken: refreshedTokens.access_token,
|
||||||
@ -108,23 +117,20 @@ export const authOptions: NextAuthOptions = {
|
|||||||
},
|
},
|
||||||
profile(profile) {
|
profile(profile) {
|
||||||
console.log('Keycloak profile callback:', {
|
console.log('Keycloak profile callback:', {
|
||||||
rawProfile: profile,
|
email: profile.email,
|
||||||
rawRoles: profile.roles,
|
name: profile.name,
|
||||||
realmAccess: profile.realm_access,
|
sub: profile.sub,
|
||||||
groups: profile.groups
|
roles: profile.realm_access?.roles || []
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get roles from realm_access
|
// Get roles from realm_access
|
||||||
const roles = profile.realm_access?.roles || [];
|
const roles = profile.realm_access?.roles || [];
|
||||||
console.log('Profile callback raw roles:', roles);
|
|
||||||
|
|
||||||
// Clean up roles by removing ROLE_ prefix and converting to lowercase
|
// Clean up roles by removing ROLE_ prefix and converting to lowercase
|
||||||
const cleanRoles = roles.map((role: string) =>
|
const cleanRoles = roles.map((role: string) =>
|
||||||
role.replace(/^ROLE_/, '').toLowerCase()
|
role.replace(/^ROLE_/, '').toLowerCase()
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('Profile callback cleaned roles:', cleanRoles);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: profile.sub,
|
id: profile.sub,
|
||||||
name: profile.name ?? profile.preferred_username,
|
name: profile.name ?? profile.preferred_username,
|
||||||
@ -141,8 +147,29 @@ export const authOptions: NextAuthOptions = {
|
|||||||
strategy: "jwt",
|
strategy: "jwt",
|
||||||
maxAge: 30 * 24 * 60 * 60, // 30 days
|
maxAge: 30 * 24 * 60 * 60, // 30 days
|
||||||
},
|
},
|
||||||
|
cookies: {
|
||||||
|
sessionToken: {
|
||||||
|
name: `next-auth.session-token`,
|
||||||
|
options: {
|
||||||
|
...DEFAULT_AUTH_COOKIE_OPTIONS,
|
||||||
|
httpOnly: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
callbackUrl: {
|
||||||
|
name: `next-auth.callback-url`,
|
||||||
|
options: DEFAULT_AUTH_COOKIE_OPTIONS
|
||||||
|
},
|
||||||
|
csrfToken: {
|
||||||
|
name: `next-auth.csrf-token`,
|
||||||
|
options: {
|
||||||
|
...DEFAULT_AUTH_COOKIE_OPTIONS,
|
||||||
|
httpOnly: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async jwt({ token, account, profile }) {
|
async jwt({ token, account, profile }) {
|
||||||
|
// Initial sign in
|
||||||
if (account && profile) {
|
if (account && profile) {
|
||||||
const keycloakProfile = profile as KeycloakProfile;
|
const keycloakProfile = profile as KeycloakProfile;
|
||||||
const roles = keycloakProfile.realm_access?.roles || [];
|
const roles = keycloakProfile.realm_access?.roles || [];
|
||||||
@ -150,33 +177,25 @@ export const authOptions: NextAuthOptions = {
|
|||||||
role.replace(/^ROLE_/, '').toLowerCase()
|
role.replace(/^ROLE_/, '').toLowerCase()
|
||||||
);
|
);
|
||||||
|
|
||||||
token.accessToken = account.access_token ?? '';
|
return {
|
||||||
token.refreshToken = account.refresh_token ?? '';
|
...token,
|
||||||
token.accessTokenExpires = account.expires_at ?? 0;
|
accessToken: account.access_token,
|
||||||
token.sub = keycloakProfile.sub;
|
refreshToken: account.refresh_token,
|
||||||
token.role = cleanRoles;
|
accessTokenExpires: account.expires_at ? account.expires_at * 1000 : 0,
|
||||||
token.username = keycloakProfile.preferred_username ?? '';
|
sub: keycloakProfile.sub,
|
||||||
token.first_name = keycloakProfile.given_name ?? '';
|
role: cleanRoles,
|
||||||
token.last_name = keycloakProfile.family_name ?? '';
|
username: keycloakProfile.preferred_username ?? '',
|
||||||
} else if (token.accessToken) {
|
first_name: keycloakProfile.given_name ?? '',
|
||||||
try {
|
last_name: keycloakProfile.family_name ?? '',
|
||||||
const decoded = jwtDecode<DecodedToken>(token.accessToken);
|
};
|
||||||
if (decoded.realm_access?.roles) {
|
|
||||||
const roles = decoded.realm_access.roles;
|
|
||||||
const cleanRoles = roles.map((role: string) =>
|
|
||||||
role.replace(/^ROLE_/, '').toLowerCase()
|
|
||||||
);
|
|
||||||
token.role = cleanRoles;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error decoding token:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Date.now() < (token.accessTokenExpires as number) * 1000) {
|
// Return the previous token if the access token has not expired
|
||||||
|
if (token.accessTokenExpires && Date.now() < token.accessTokenExpires) {
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Access token has expired, try to refresh it
|
||||||
return refreshAccessToken(token);
|
return refreshAccessToken(token);
|
||||||
},
|
},
|
||||||
async session({ session, token }) {
|
async session({ session, token }) {
|
||||||
@ -206,21 +225,21 @@ export const authOptions: NextAuthOptions = {
|
|||||||
error: '/signin',
|
error: '/signin',
|
||||||
},
|
},
|
||||||
debug: process.env.NODE_ENV === 'development',
|
debug: process.env.NODE_ENV === 'development',
|
||||||
|
logger: {
|
||||||
|
error(code, metadata) {
|
||||||
|
console.error(`Auth error: ${code}`, metadata);
|
||||||
|
},
|
||||||
|
warn(code) {
|
||||||
|
console.warn(`Auth warning: ${code}`);
|
||||||
|
},
|
||||||
|
debug(code, metadata) {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.debug(`Auth debug: ${code}`, metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handler = NextAuth(authOptions);
|
const handler = NextAuth(authOptions);
|
||||||
export { handler as GET, handler as POST };
|
export { handler as GET, handler as POST };
|
||||||
|
|
||||||
interface JWT {
|
|
||||||
accessToken: string;
|
|
||||||
refreshToken: string;
|
|
||||||
accessTokenExpires: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Profile {
|
|
||||||
sub?: string;
|
|
||||||
email?: string;
|
|
||||||
name?: string;
|
|
||||||
roles?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
123
app/api/keycloak/token/route.ts
Normal file
123
app/api/keycloak/token/route.ts
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { getServerSession } from "next-auth/next";
|
||||||
|
import { authOptions } from "../../auth/[...nextauth]/route";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for getting a Keycloak admin token
|
||||||
|
* This replaces the previous proxy implementation
|
||||||
|
*/
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
|
// Check authentication
|
||||||
|
if (!session || !session.user) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Unauthorized" },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has admin role
|
||||||
|
const isAdmin = session.user.role?.includes('admin');
|
||||||
|
if (!isAdmin) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Forbidden: Admin role required" },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get admin token
|
||||||
|
const tokenResponse = await fetch(
|
||||||
|
`${process.env.KEYCLOAK_ISSUER}/protocol/openid-connect/token`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
grant_type: "client_credentials",
|
||||||
|
client_id: process.env.KEYCLOAK_CLIENT_ID!,
|
||||||
|
client_secret: process.env.KEYCLOAK_CLIENT_SECRET!,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await tokenResponse.json();
|
||||||
|
|
||||||
|
if (!tokenResponse.ok || !data.access_token) {
|
||||||
|
console.error("Token Error:", data);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to get admin token" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the token
|
||||||
|
return NextResponse.json({
|
||||||
|
access_token: data.access_token,
|
||||||
|
expires_in: data.expires_in,
|
||||||
|
token_type: data.token_type
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Token Error:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Server error obtaining token" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle refreshing a token
|
||||||
|
*/
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { refresh_token } = await request.json();
|
||||||
|
|
||||||
|
if (!refresh_token) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Refresh token is required" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.KEYCLOAK_ISSUER}/protocol/openid-connect/token`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
client_id: process.env.KEYCLOAK_CLIENT_ID!,
|
||||||
|
client_secret: process.env.KEYCLOAK_CLIENT_SECRET!,
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
refresh_token: refresh_token,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Token refresh failed", details: data },
|
||||||
|
{ status: response.status }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
access_token: data.access_token,
|
||||||
|
refresh_token: data.refresh_token,
|
||||||
|
expires_in: data.expires_in,
|
||||||
|
token_type: data.token_type
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Token refresh error:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Server error refreshing token" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
265
app/api/keycloak/users/route.ts
Normal file
265
app/api/keycloak/users/route.ts
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { getServerSession } from "next-auth/next";
|
||||||
|
import { authOptions } from "../../auth/[...nextauth]/route";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an admin token
|
||||||
|
*/
|
||||||
|
async function getAdminToken() {
|
||||||
|
try {
|
||||||
|
const tokenResponse = await fetch(
|
||||||
|
`${process.env.KEYCLOAK_ISSUER}/protocol/openid-connect/token`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
grant_type: "client_credentials",
|
||||||
|
client_id: process.env.KEYCLOAK_CLIENT_ID!,
|
||||||
|
client_secret: process.env.KEYCLOAK_CLIENT_SECRET!,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await tokenResponse.json();
|
||||||
|
|
||||||
|
if (!tokenResponse.ok || !data.access_token) {
|
||||||
|
console.error("Token Error:", data);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.access_token;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Token Error:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get users from Keycloak
|
||||||
|
*/
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
|
// Check authentication
|
||||||
|
if (!session || !session.user) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Unauthorized" },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only administrators can list users
|
||||||
|
const isAdmin = session.user.role?.includes('admin');
|
||||||
|
if (!isAdmin) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Forbidden: Admin role required" },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get admin token
|
||||||
|
const token = await getAdminToken();
|
||||||
|
if (!token) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to get admin token" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse search params
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const query = searchParams.get('query') || '';
|
||||||
|
const max = searchParams.get('max') || '100';
|
||||||
|
|
||||||
|
// Fetch users from Keycloak
|
||||||
|
const keycloakUrl = `${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users`;
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
queryParams.append('search', query);
|
||||||
|
}
|
||||||
|
|
||||||
|
queryParams.append('max', max);
|
||||||
|
|
||||||
|
const usersResponse = await fetch(
|
||||||
|
`${keycloakUrl}?${queryParams.toString()}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!usersResponse.ok) {
|
||||||
|
const errorData = await usersResponse.json();
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to fetch users", details: errorData },
|
||||||
|
{ status: usersResponse.status }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = await usersResponse.json();
|
||||||
|
|
||||||
|
// Return user data
|
||||||
|
return NextResponse.json(users);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching users:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Server error fetching users" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new user in Keycloak
|
||||||
|
*/
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
|
// Check authentication
|
||||||
|
if (!session || !session.user) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Unauthorized" },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only administrators can create users
|
||||||
|
const isAdmin = session.user.role?.includes('admin');
|
||||||
|
if (!isAdmin) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Forbidden: Admin role required" },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get admin token
|
||||||
|
const token = await getAdminToken();
|
||||||
|
if (!token) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to get admin token" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user data from request
|
||||||
|
const userData = await request.json();
|
||||||
|
|
||||||
|
// Validate user data
|
||||||
|
if (!userData.username || !userData.email) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Username and email are required" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user in Keycloak
|
||||||
|
const keycloakUrl = `${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users`;
|
||||||
|
const createResponse = await fetch(keycloakUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: userData.username,
|
||||||
|
email: userData.email,
|
||||||
|
enabled: true,
|
||||||
|
emailVerified: true,
|
||||||
|
firstName: userData.firstName || '',
|
||||||
|
lastName: userData.lastName || '',
|
||||||
|
credentials: userData.password ? [
|
||||||
|
{
|
||||||
|
type: "password",
|
||||||
|
value: userData.password,
|
||||||
|
temporary: false
|
||||||
|
}
|
||||||
|
] : undefined
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!createResponse.ok) {
|
||||||
|
const errorData = await createResponse.json().catch(() => ({}));
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to create user", details: errorData },
|
||||||
|
{ status: createResponse.status }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user was created successfully, get the user ID
|
||||||
|
const userResponse = await fetch(
|
||||||
|
`${keycloakUrl}?username=${encodeURIComponent(userData.username)}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!userResponse.ok) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "User created but failed to retrieve user details" },
|
||||||
|
{ status: 207 } // Partial success
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = await userResponse.json();
|
||||||
|
const createdUser = users[0];
|
||||||
|
|
||||||
|
// If roles are specified, assign them
|
||||||
|
if (userData.roles && Array.isArray(userData.roles) && userData.roles.length > 0) {
|
||||||
|
// Get available roles
|
||||||
|
const rolesResponse = await fetch(
|
||||||
|
`${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/roles`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rolesResponse.ok) {
|
||||||
|
const availableRoles = await rolesResponse.json();
|
||||||
|
|
||||||
|
// Filter valid roles
|
||||||
|
const validRoles = userData.roles.map((roleName: string) =>
|
||||||
|
availableRoles.find((r: any) => r.name === roleName)
|
||||||
|
).filter(Boolean);
|
||||||
|
|
||||||
|
if (validRoles.length > 0) {
|
||||||
|
// Assign roles to user
|
||||||
|
await fetch(
|
||||||
|
`${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users/${createdUser.id}/role-mappings/realm`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(validRoles),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
user: createdUser
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating user:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Server error creating user" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,166 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
import { getServerSession } from 'next-auth/next';
|
|
||||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
|
||||||
|
|
||||||
// Map of service prefixes to their base URLs
|
|
||||||
const SERVICE_URLS: Record<string, string> = {
|
|
||||||
'parole': process.env.NEXT_PUBLIC_IFRAME_PAROLE_URL || '',
|
|
||||||
'alma': process.env.NEXT_PUBLIC_IFRAME_AI_ASSISTANT_URL || '',
|
|
||||||
'crm': process.env.NEXT_PUBLIC_IFRAME_MEDIATIONS_URL || '',
|
|
||||||
'vision': process.env.NEXT_PUBLIC_IFRAME_CONFERENCE_URL || '',
|
|
||||||
'showcase': process.env.NEXT_PUBLIC_IFRAME_SHOWCASE_URL || '',
|
|
||||||
'agilite': process.env.NEXT_PUBLIC_IFRAME_AGILITY_URL || '',
|
|
||||||
'dossiers': process.env.NEXT_PUBLIC_IFRAME_DRIVE_URL || '',
|
|
||||||
'the-message': process.env.NEXT_PUBLIC_IFRAME_THEMESSAGE_URL || '',
|
|
||||||
'qg': process.env.NEXT_PUBLIC_IFRAME_MISSIONVIEW_URL || ''
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if a service is Rocket.Chat (they require special authentication)
|
|
||||||
function isRocketChat(serviceName: string): boolean {
|
|
||||||
return serviceName === 'parole'; // Assuming 'parole' is your Rocket.Chat service
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
context: { params: { path: string[] } }
|
|
||||||
) {
|
|
||||||
// Get the service prefix (first part of the path)
|
|
||||||
const paramsObj = await Promise.resolve(context.params);
|
|
||||||
const pathArray = await Promise.resolve(paramsObj.path);
|
|
||||||
|
|
||||||
const serviceName = pathArray[0];
|
|
||||||
const restOfPath = pathArray.slice(1).join('/');
|
|
||||||
|
|
||||||
// Get the base URL for this service
|
|
||||||
const baseUrl = SERVICE_URLS[serviceName];
|
|
||||||
if (!baseUrl) {
|
|
||||||
return NextResponse.json({ error: 'Service not found' }, { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the user's session
|
|
||||||
const session = await getServerSession(authOptions);
|
|
||||||
if (!session) {
|
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Extract search parameters
|
|
||||||
const searchParams = new URL(request.url).searchParams.toString();
|
|
||||||
const targetUrl = `${baseUrl}/${restOfPath}${searchParams ? `?${searchParams}` : ''}`;
|
|
||||||
|
|
||||||
// Prepare headers based on the service type
|
|
||||||
const headers: Record<string, string> = {};
|
|
||||||
|
|
||||||
if (isRocketChat(serviceName)) {
|
|
||||||
// For Rocket.Chat, use their specific authentication headers
|
|
||||||
if (session.rocketChatToken && session.rocketChatUserId) {
|
|
||||||
console.log('Using Rocket.Chat specific authentication');
|
|
||||||
headers['X-Auth-Token'] = session.rocketChatToken;
|
|
||||||
headers['X-User-Id'] = session.rocketChatUserId;
|
|
||||||
} else {
|
|
||||||
console.warn('Rocket.Chat tokens not available in session');
|
|
||||||
// Still try with standard authorization if available
|
|
||||||
if (session.accessToken) {
|
|
||||||
headers['Authorization'] = `Bearer ${session.accessToken}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Standard OAuth Bearer token for other services
|
|
||||||
if (session.accessToken) {
|
|
||||||
headers['Authorization'] = `Bearer ${session.accessToken}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add other common headers
|
|
||||||
headers['Accept'] = 'application/json, text/html, */*';
|
|
||||||
|
|
||||||
// Forward the request to the target service with the authentication headers
|
|
||||||
const response = await fetch(targetUrl, { headers });
|
|
||||||
|
|
||||||
// Get response data
|
|
||||||
const data = await response.arrayBuffer();
|
|
||||||
|
|
||||||
// Create response with the same status and headers
|
|
||||||
const newResponse = new NextResponse(data, {
|
|
||||||
status: response.status,
|
|
||||||
statusText: response.statusText,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': response.headers.get('Content-Type') || 'application/octet-stream',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return newResponse;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Proxy error:', error);
|
|
||||||
return NextResponse.json({ error: 'Proxy error' }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(
|
|
||||||
request: NextRequest,
|
|
||||||
context: { params: { path: string[] } }
|
|
||||||
) {
|
|
||||||
// Get the service prefix (first part of the path)
|
|
||||||
const paramsObj = await Promise.resolve(context.params);
|
|
||||||
const pathArray = await Promise.resolve(paramsObj.path);
|
|
||||||
|
|
||||||
const serviceName = pathArray[0];
|
|
||||||
const restOfPath = pathArray.slice(1).join('/');
|
|
||||||
|
|
||||||
const baseUrl = SERVICE_URLS[serviceName];
|
|
||||||
if (!baseUrl) {
|
|
||||||
return NextResponse.json({ error: 'Service not found' }, { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = await getServerSession(authOptions);
|
|
||||||
if (!session) {
|
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const searchParams = new URL(request.url).searchParams.toString();
|
|
||||||
const targetUrl = `${baseUrl}/${restOfPath}${searchParams ? `?${searchParams}` : ''}`;
|
|
||||||
|
|
||||||
// Get the request body
|
|
||||||
const body = await request.arrayBuffer();
|
|
||||||
|
|
||||||
// Prepare headers based on the service type
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
'Content-Type': request.headers.get('Content-Type') || 'application/json',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isRocketChat(serviceName)) {
|
|
||||||
// For Rocket.Chat, use their specific authentication headers
|
|
||||||
if (session.rocketChatToken && session.rocketChatUserId) {
|
|
||||||
headers['X-Auth-Token'] = session.rocketChatToken;
|
|
||||||
headers['X-User-Id'] = session.rocketChatUserId;
|
|
||||||
} else if (session.accessToken) {
|
|
||||||
headers['Authorization'] = `Bearer ${session.accessToken}`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Standard OAuth Bearer token for other services
|
|
||||||
if (session.accessToken) {
|
|
||||||
headers['Authorization'] = `Bearer ${session.accessToken}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(targetUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers,
|
|
||||||
body: body
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.arrayBuffer();
|
|
||||||
|
|
||||||
return new NextResponse(data, {
|
|
||||||
status: response.status,
|
|
||||||
statusText: response.statusText,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': response.headers.get('Content-Type') || 'application/octet-stream',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Proxy error:', error);
|
|
||||||
return NextResponse.json({ error: 'Proxy error' }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { clearAuthCookies } from "@/lib/session";
|
import { clearAuthCookiesClient } from "@/lib/cookies";
|
||||||
|
|
||||||
export function SignOutHandler() {
|
export function SignOutHandler() {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
@ -11,7 +11,7 @@ export function SignOutHandler() {
|
|||||||
const handleSignOut = async () => {
|
const handleSignOut = async () => {
|
||||||
try {
|
try {
|
||||||
// First, clear all auth-related cookies to ensure we break any local sessions
|
// First, clear all auth-related cookies to ensure we break any local sessions
|
||||||
clearAuthCookies();
|
clearAuthCookiesClient();
|
||||||
|
|
||||||
// Get Keycloak logout URL
|
// Get Keycloak logout URL
|
||||||
if (process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER) {
|
if (process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER) {
|
||||||
|
|||||||
193
lib/cookies.ts
Normal file
193
lib/cookies.ts
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
import { serialize, parse } from 'cookie';
|
||||||
|
import { IncomingMessage } from 'http';
|
||||||
|
import { NextApiRequestCookies } from 'next/dist/server/api-utils';
|
||||||
|
|
||||||
|
export interface CookieOptions {
|
||||||
|
maxAge?: number;
|
||||||
|
expires?: Date;
|
||||||
|
path?: string;
|
||||||
|
domain?: string;
|
||||||
|
secure?: boolean;
|
||||||
|
httpOnly?: boolean;
|
||||||
|
sameSite?: 'strict' | 'lax' | 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default cookie options for auth-related cookies
|
||||||
|
export const DEFAULT_AUTH_COOKIE_OPTIONS: CookieOptions = {
|
||||||
|
maxAge: 30 * 24 * 60 * 60, // 30 days
|
||||||
|
path: '/',
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: 'lax'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cookie names
|
||||||
|
export const COOKIE_NAMES = {
|
||||||
|
// NextAuth cookies
|
||||||
|
AUTH_TOKEN: 'next-auth.session-token',
|
||||||
|
AUTH_CSRF_TOKEN: 'next-auth.csrf-token',
|
||||||
|
AUTH_CALLBACK_URL: 'next-auth.callback-url',
|
||||||
|
AUTH_PKCE_CODE_CHALLENGE: 'next-auth.pkce.code_challenge',
|
||||||
|
|
||||||
|
// Keycloak cookies
|
||||||
|
KEYCLOAK_SESSION: 'KEYCLOAK_SESSION',
|
||||||
|
KEYCLOAK_IDENTITY: 'KEYCLOAK_IDENTITY',
|
||||||
|
KEYCLOAK_REMEMBER_ME: 'KEYCLOAK_REMEMBER_ME',
|
||||||
|
KC_RESTART: 'KC_RESTART',
|
||||||
|
|
||||||
|
// Custom cookies
|
||||||
|
USER_PREFERENCES: 'user-preferences',
|
||||||
|
THEME: 'theme',
|
||||||
|
|
||||||
|
// Function to create a namespaced cookie name
|
||||||
|
namespaced: (name: string) => `neah-front9.${name}`
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a cookie with the specified options
|
||||||
|
*/
|
||||||
|
export function setCookie(
|
||||||
|
name: string,
|
||||||
|
value: string,
|
||||||
|
options: CookieOptions = {}
|
||||||
|
): string {
|
||||||
|
// Merge with default options
|
||||||
|
const cookieOptions = {
|
||||||
|
...DEFAULT_AUTH_COOKIE_OPTIONS,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
// For security, ensure secure flag is set if sameSite is 'none'
|
||||||
|
if (cookieOptions.sameSite === 'none' && cookieOptions.secure === undefined) {
|
||||||
|
cookieOptions.secure = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the cookie string
|
||||||
|
return serialize(name, value, cookieOptions as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all cookies from the request
|
||||||
|
*/
|
||||||
|
export function getCookies(req: {
|
||||||
|
headers: { cookie?: string };
|
||||||
|
}): Record<string, string> {
|
||||||
|
const cookie = req.headers?.cookie;
|
||||||
|
return parse(cookie || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific cookie value
|
||||||
|
*/
|
||||||
|
export function getCookie(
|
||||||
|
req: { headers: { cookie?: string } },
|
||||||
|
name: string
|
||||||
|
): string | undefined {
|
||||||
|
const cookies = getCookies(req);
|
||||||
|
return cookies[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a cookie by setting its expiration to the past
|
||||||
|
*/
|
||||||
|
export function deleteCookie(
|
||||||
|
name: string,
|
||||||
|
options: CookieOptions = {}
|
||||||
|
): string {
|
||||||
|
return setCookie(name, '', {
|
||||||
|
...options,
|
||||||
|
maxAge: 0,
|
||||||
|
expires: new Date(0)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to generate SetCookie headers for multiple cookies
|
||||||
|
*/
|
||||||
|
export function createCookieHeaders(cookieStrings: string[]): [string, string][] {
|
||||||
|
return cookieStrings.map(cookie => ['Set-Cookie', cookie]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all auth-related cookies
|
||||||
|
*/
|
||||||
|
export function getAuthCookieClearingHeaders(): [string, string][] {
|
||||||
|
const authCookies = [
|
||||||
|
COOKIE_NAMES.AUTH_TOKEN,
|
||||||
|
COOKIE_NAMES.AUTH_CSRF_TOKEN,
|
||||||
|
COOKIE_NAMES.AUTH_CALLBACK_URL,
|
||||||
|
COOKIE_NAMES.AUTH_PKCE_CODE_CHALLENGE,
|
||||||
|
COOKIE_NAMES.KEYCLOAK_SESSION,
|
||||||
|
COOKIE_NAMES.KEYCLOAK_IDENTITY,
|
||||||
|
COOKIE_NAMES.KEYCLOAK_REMEMBER_ME,
|
||||||
|
COOKIE_NAMES.KC_RESTART,
|
||||||
|
// Also clear secure variants
|
||||||
|
`__Secure-${COOKIE_NAMES.AUTH_TOKEN}`,
|
||||||
|
`__Host-${COOKIE_NAMES.AUTH_TOKEN}`
|
||||||
|
];
|
||||||
|
|
||||||
|
// Create clearing headers for root path
|
||||||
|
const cookieHeaders = authCookies.flatMap(name => {
|
||||||
|
return [
|
||||||
|
deleteCookie(name, { path: '/' }),
|
||||||
|
deleteCookie(name, { path: '/auth' }),
|
||||||
|
deleteCookie(name, { path: '/api' })
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return createCookieHeaders(cookieHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client-side function to clear all auth cookies
|
||||||
|
*/
|
||||||
|
export function clearAuthCookiesClient(): void {
|
||||||
|
const authCookiePrefixes = [
|
||||||
|
'next-auth.',
|
||||||
|
'__Secure-next-auth.',
|
||||||
|
'__Host-next-auth.',
|
||||||
|
'KEYCLOAK_',
|
||||||
|
'KC_'
|
||||||
|
];
|
||||||
|
|
||||||
|
const specificCookies = [
|
||||||
|
COOKIE_NAMES.KEYCLOAK_SESSION,
|
||||||
|
COOKIE_NAMES.KEYCLOAK_IDENTITY,
|
||||||
|
COOKIE_NAMES.KEYCLOAK_REMEMBER_ME,
|
||||||
|
COOKIE_NAMES.KC_RESTART
|
||||||
|
];
|
||||||
|
|
||||||
|
const cookies = document.cookie.split(';');
|
||||||
|
|
||||||
|
for (const cookie of cookies) {
|
||||||
|
const [name] = cookie.split('=');
|
||||||
|
const trimmedName = name.trim();
|
||||||
|
|
||||||
|
const isAuthCookie =
|
||||||
|
authCookiePrefixes.some(prefix => trimmedName.startsWith(prefix)) ||
|
||||||
|
specificCookies.includes(trimmedName);
|
||||||
|
|
||||||
|
if (isAuthCookie) {
|
||||||
|
// Clear cookie for different paths and domains
|
||||||
|
const paths = ['/', '/auth', '/api'];
|
||||||
|
|
||||||
|
for (const path of paths) {
|
||||||
|
// Basic deletion
|
||||||
|
document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path};`;
|
||||||
|
|
||||||
|
// With Secure and SameSite
|
||||||
|
document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; SameSite=None; Secure;`;
|
||||||
|
|
||||||
|
// Try with domain
|
||||||
|
const domain = window.location.hostname;
|
||||||
|
document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; domain=${domain};`;
|
||||||
|
document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; domain=${domain}; SameSite=None; Secure;`;
|
||||||
|
|
||||||
|
// Try with root domain
|
||||||
|
const rootDomain = `.${domain.split('.').slice(-2).join('.')}`;
|
||||||
|
document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; domain=${rootDomain};`;
|
||||||
|
document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; domain=${rootDomain}; SameSite=None; Secure;`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
88
lib/redis.ts
88
lib/redis.ts
@ -127,7 +127,11 @@ export const KEYS = {
|
|||||||
EMAIL_LIST: (userId: string, accountId: string, folder: string, page: number, perPage: number) =>
|
EMAIL_LIST: (userId: string, accountId: string, folder: string, page: number, perPage: number) =>
|
||||||
`email:list:${userId}:${accountId}:${folder}:${page}:${perPage}`,
|
`email:list:${userId}:${accountId}:${folder}:${page}:${perPage}`,
|
||||||
EMAIL_CONTENT: (userId: string, accountId: string, emailId: string) =>
|
EMAIL_CONTENT: (userId: string, accountId: string, emailId: string) =>
|
||||||
`email:content:${userId}:${accountId}:${emailId}`
|
`email:content:${userId}:${accountId}:${emailId}`,
|
||||||
|
// Add auth-specific Redis keys
|
||||||
|
AUTH_STATE: (sessionId: string) => `auth:state:${sessionId}`,
|
||||||
|
AUTH_REFRESH_TOKEN: (userId: string) => `auth:refresh:${userId}`,
|
||||||
|
AUTH_SESSION: (sessionId: string) => `auth:session:${sessionId}`
|
||||||
};
|
};
|
||||||
|
|
||||||
// TTL constants in seconds
|
// TTL constants in seconds
|
||||||
@ -135,7 +139,10 @@ export const TTL = {
|
|||||||
CREDENTIALS: 60 * 60 * 24, // 24 hours
|
CREDENTIALS: 60 * 60 * 24, // 24 hours
|
||||||
SESSION: 60 * 60 * 4, // 4 hours (increased from 30 minutes)
|
SESSION: 60 * 60 * 4, // 4 hours (increased from 30 minutes)
|
||||||
EMAIL_LIST: 60 * 5, // 5 minutes
|
EMAIL_LIST: 60 * 5, // 5 minutes
|
||||||
EMAIL_CONTENT: 60 * 15 // 15 minutes
|
EMAIL_CONTENT: 60 * 15, // 15 minutes
|
||||||
|
AUTH_STATE: 60 * 10, // 10 minutes
|
||||||
|
AUTH_REFRESH_TOKEN: 60 * 60 * 24 * 30, // 30 days
|
||||||
|
AUTH_SESSION: 60 * 60 * 24 * 7 // 7 days
|
||||||
};
|
};
|
||||||
|
|
||||||
interface EmailCredentials {
|
interface EmailCredentials {
|
||||||
@ -494,4 +501,81 @@ export async function getCachedEmailCredentials(
|
|||||||
accountId: string
|
accountId: string
|
||||||
): Promise<EmailCredentials | null> {
|
): Promise<EmailCredentials | null> {
|
||||||
return getEmailCredentials(userId, accountId);
|
return getEmailCredentials(userId, accountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache an auth state for PKCE
|
||||||
|
*/
|
||||||
|
export async function cacheAuthState(
|
||||||
|
sessionId: string,
|
||||||
|
state: string
|
||||||
|
): Promise<void> {
|
||||||
|
const redis = getRedisClient();
|
||||||
|
const key = KEYS.AUTH_STATE(sessionId);
|
||||||
|
await redis.set(key, state, 'EX', TTL.AUTH_STATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a cached auth state
|
||||||
|
*/
|
||||||
|
export async function getAuthState(
|
||||||
|
sessionId: string
|
||||||
|
): Promise<string | null> {
|
||||||
|
const redis = getRedisClient();
|
||||||
|
const key = KEYS.AUTH_STATE(sessionId);
|
||||||
|
return redis.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete auth state after use
|
||||||
|
*/
|
||||||
|
export async function deleteAuthState(
|
||||||
|
sessionId: string
|
||||||
|
): Promise<void> {
|
||||||
|
const redis = getRedisClient();
|
||||||
|
const key = KEYS.AUTH_STATE(sessionId);
|
||||||
|
await redis.del(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache a refresh token
|
||||||
|
*/
|
||||||
|
export async function cacheRefreshToken(
|
||||||
|
userId: string,
|
||||||
|
refreshToken: string
|
||||||
|
): Promise<void> {
|
||||||
|
const redis = getRedisClient();
|
||||||
|
const key = KEYS.AUTH_REFRESH_TOKEN(userId);
|
||||||
|
await redis.set(key, encryptData(refreshToken), 'EX', TTL.AUTH_REFRESH_TOKEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a cached refresh token
|
||||||
|
*/
|
||||||
|
export async function getRefreshToken(
|
||||||
|
userId: string
|
||||||
|
): Promise<string | null> {
|
||||||
|
const redis = getRedisClient();
|
||||||
|
const key = KEYS.AUTH_REFRESH_TOKEN(userId);
|
||||||
|
const token = await redis.get(key);
|
||||||
|
|
||||||
|
if (!token) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return decryptData(token);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error decrypting refresh token:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a refresh token
|
||||||
|
*/
|
||||||
|
export async function deleteRefreshToken(
|
||||||
|
userId: string
|
||||||
|
): Promise<void> {
|
||||||
|
const redis = getRedisClient();
|
||||||
|
const key = KEYS.AUTH_REFRESH_TOKEN(userId);
|
||||||
|
await redis.del(key);
|
||||||
}
|
}
|
||||||
113
middleware.ts
Normal file
113
middleware.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
import { getToken } from 'next-auth/jwt';
|
||||||
|
|
||||||
|
// Define paths that don't require authentication
|
||||||
|
const publicPaths = [
|
||||||
|
'/api/auth',
|
||||||
|
'/signin',
|
||||||
|
'/loggedout',
|
||||||
|
'/register',
|
||||||
|
'/error',
|
||||||
|
'/images',
|
||||||
|
'/fonts',
|
||||||
|
'/favicon.ico',
|
||||||
|
'/robots.txt',
|
||||||
|
'/sitemap.xml',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check if the requested path is public
|
||||||
|
function isPublicPath(path: string): boolean {
|
||||||
|
return publicPaths.some(publicPath => path.startsWith(publicPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function middleware(request: NextRequest) {
|
||||||
|
const { pathname } = request.nextUrl;
|
||||||
|
|
||||||
|
// Check if the path is public (no authentication needed)
|
||||||
|
if (isPublicPath(pathname)) {
|
||||||
|
return NextResponse.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// For API routes, check for token in the request
|
||||||
|
if (pathname.startsWith('/api/')) {
|
||||||
|
try {
|
||||||
|
// Verify authentication token
|
||||||
|
const token = await getToken({
|
||||||
|
req: request,
|
||||||
|
secret: process.env.NEXTAUTH_SECRET
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
// No token found, redirect to sign-in page or return 401 for API requests
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Authentication required' },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token is expired
|
||||||
|
if (token.accessTokenExpires && (token.accessTokenExpires as number) < Date.now()) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Session expired' },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token is valid, proceed
|
||||||
|
return NextResponse.next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Auth middleware error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Authentication error' },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For page routes, verify session
|
||||||
|
try {
|
||||||
|
const token = await getToken({
|
||||||
|
req: request,
|
||||||
|
secret: process.env.NEXTAUTH_SECRET
|
||||||
|
});
|
||||||
|
|
||||||
|
// If no token found, redirect to sign-in page
|
||||||
|
if (!token) {
|
||||||
|
const url = new URL('/signin', request.url);
|
||||||
|
url.searchParams.set('callbackUrl', encodeURI(request.url));
|
||||||
|
return NextResponse.redirect(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token is expired
|
||||||
|
if (token.accessTokenExpires && (token.accessTokenExpires as number) < Date.now()) {
|
||||||
|
const url = new URL('/signin', request.url);
|
||||||
|
url.searchParams.set('callbackUrl', encodeURI(request.url));
|
||||||
|
url.searchParams.set('error', 'SessionExpired');
|
||||||
|
return NextResponse.redirect(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If authorized, proceed to the requested page
|
||||||
|
return NextResponse.next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Auth middleware error:', error);
|
||||||
|
const url = new URL('/signin', request.url);
|
||||||
|
url.searchParams.set('error', 'AuthError');
|
||||||
|
return NextResponse.redirect(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only run middleware on matching paths
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
/*
|
||||||
|
* Match all request paths except:
|
||||||
|
* 1. /_next (Next.js internals)
|
||||||
|
* 2. /static (static files)
|
||||||
|
* 3. /images (public images)
|
||||||
|
* 4. /fonts (public fonts)
|
||||||
|
* 5. /favicon.ico, /robots.txt, /sitemap.xml (SEO files)
|
||||||
|
*/
|
||||||
|
'/((?!_next/|static/|images/|fonts/|favicon.ico|robots.txt|sitemap.xml).*)',
|
||||||
|
],
|
||||||
|
};
|
||||||
22
node_modules/.package-lock.json
generated
vendored
22
node_modules/.package-lock.json
generated
vendored
@ -2337,6 +2337,13 @@
|
|||||||
"tslib": "^2.8.0"
|
"tslib": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/cookie": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/crypto-js": {
|
"node_modules/@types/crypto-js": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
|
||||||
@ -3065,9 +3072,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cookie": {
|
"node_modules/cookie": {
|
||||||
"version": "0.7.2",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
@ -4958,6 +4965,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/next-auth/node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/next-themes": {
|
"node_modules/next-themes": {
|
||||||
"version": "0.4.4",
|
"version": "0.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.4.tgz",
|
||||||
|
|||||||
183
node_modules/cookie/index.js
generated
vendored
183
node_modules/cookie/index.js
generated
vendored
@ -21,69 +21,16 @@ exports.serialize = serialize;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
var __toString = Object.prototype.toString
|
var __toString = Object.prototype.toString
|
||||||
var __hasOwnProperty = Object.prototype.hasOwnProperty
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RegExp to match cookie-name in RFC 6265 sec 4.1.1
|
* RegExp to match field-content in RFC 7230 sec 3.2
|
||||||
* This refers out to the obsoleted definition of token in RFC 2616 sec 2.2
|
|
||||||
* which has been replaced by the token definition in RFC 7230 appendix B.
|
|
||||||
*
|
*
|
||||||
* cookie-name = token
|
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
|
||||||
* token = 1*tchar
|
* field-vchar = VCHAR / obs-text
|
||||||
* tchar = "!" / "#" / "$" / "%" / "&" / "'" /
|
* obs-text = %x80-FF
|
||||||
* "*" / "+" / "-" / "." / "^" / "_" /
|
|
||||||
* "`" / "|" / "~" / DIGIT / ALPHA
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var cookieNameRegExp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;
|
||||||
|
|
||||||
/**
|
|
||||||
* RegExp to match cookie-value in RFC 6265 sec 4.1.1
|
|
||||||
*
|
|
||||||
* cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
|
|
||||||
* cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
|
|
||||||
* ; US-ASCII characters excluding CTLs,
|
|
||||||
* ; whitespace DQUOTE, comma, semicolon,
|
|
||||||
* ; and backslash
|
|
||||||
*/
|
|
||||||
|
|
||||||
var cookieValueRegExp = /^("?)[\u0021\u0023-\u002B\u002D-\u003A\u003C-\u005B\u005D-\u007E]*\1$/;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RegExp to match domain-value in RFC 6265 sec 4.1.1
|
|
||||||
*
|
|
||||||
* domain-value = <subdomain>
|
|
||||||
* ; defined in [RFC1034], Section 3.5, as
|
|
||||||
* ; enhanced by [RFC1123], Section 2.1
|
|
||||||
* <subdomain> = <label> | <subdomain> "." <label>
|
|
||||||
* <label> = <let-dig> [ [ <ldh-str> ] <let-dig> ]
|
|
||||||
* Labels must be 63 characters or less.
|
|
||||||
* 'let-dig' not 'letter' in the first char, per RFC1123
|
|
||||||
* <ldh-str> = <let-dig-hyp> | <let-dig-hyp> <ldh-str>
|
|
||||||
* <let-dig-hyp> = <let-dig> | "-"
|
|
||||||
* <let-dig> = <letter> | <digit>
|
|
||||||
* <letter> = any one of the 52 alphabetic characters A through Z in
|
|
||||||
* upper case and a through z in lower case
|
|
||||||
* <digit> = any one of the ten digits 0 through 9
|
|
||||||
*
|
|
||||||
* Keep support for leading dot: https://github.com/jshttp/cookie/issues/173
|
|
||||||
*
|
|
||||||
* > (Note that a leading %x2E ("."), if present, is ignored even though that
|
|
||||||
* character is not permitted, but a trailing %x2E ("."), if present, will
|
|
||||||
* cause the user agent to ignore the attribute.)
|
|
||||||
*/
|
|
||||||
|
|
||||||
var domainValueRegExp = /^([.]?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)([.][a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RegExp to match path-value in RFC 6265 sec 4.1.1
|
|
||||||
*
|
|
||||||
* path-value = <any CHAR except CTLs or ";">
|
|
||||||
* CHAR = %x01-7F
|
|
||||||
* ; defined in RFC 5234 appendix B.1
|
|
||||||
*/
|
|
||||||
|
|
||||||
var pathValueRegExp = /^[\u0020-\u003A\u003D-\u007E]*$/;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a cookie header.
|
* Parse a cookie header.
|
||||||
@ -92,128 +39,107 @@ var pathValueRegExp = /^[\u0020-\u003A\u003D-\u007E]*$/;
|
|||||||
* The object has the various cookies as keys(names) => values
|
* The object has the various cookies as keys(names) => values
|
||||||
*
|
*
|
||||||
* @param {string} str
|
* @param {string} str
|
||||||
* @param {object} [opt]
|
* @param {object} [options]
|
||||||
* @return {object}
|
* @return {object}
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function parse(str, opt) {
|
function parse(str, options) {
|
||||||
if (typeof str !== 'string') {
|
if (typeof str !== 'string') {
|
||||||
throw new TypeError('argument str must be a string');
|
throw new TypeError('argument str must be a string');
|
||||||
}
|
}
|
||||||
|
|
||||||
var obj = {};
|
var obj = {}
|
||||||
var len = str.length;
|
var opt = options || {};
|
||||||
// RFC 6265 sec 4.1.1, RFC 2616 2.2 defines a cookie name consists of one char minimum, plus '='.
|
var dec = opt.decode || decode;
|
||||||
if (len < 2) return obj;
|
|
||||||
|
|
||||||
var dec = (opt && opt.decode) || decode;
|
var index = 0
|
||||||
var index = 0;
|
while (index < str.length) {
|
||||||
var eqIdx = 0;
|
var eqIdx = str.indexOf('=', index)
|
||||||
var endIdx = 0;
|
|
||||||
|
|
||||||
do {
|
// no more cookie pairs
|
||||||
eqIdx = str.indexOf('=', index);
|
if (eqIdx === -1) {
|
||||||
if (eqIdx === -1) break; // No more cookie pairs.
|
break
|
||||||
|
|
||||||
endIdx = str.indexOf(';', index);
|
|
||||||
|
|
||||||
if (endIdx === -1) {
|
|
||||||
endIdx = len;
|
|
||||||
} else if (eqIdx > endIdx) {
|
|
||||||
// backtrack on prior semicolon
|
|
||||||
index = str.lastIndexOf(';', eqIdx - 1) + 1;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var keyStartIdx = startIndex(str, index, eqIdx);
|
var endIdx = str.indexOf(';', index)
|
||||||
var keyEndIdx = endIndex(str, eqIdx, keyStartIdx);
|
|
||||||
var key = str.slice(keyStartIdx, keyEndIdx);
|
if (endIdx === -1) {
|
||||||
|
endIdx = str.length
|
||||||
|
} else if (endIdx < eqIdx) {
|
||||||
|
// backtrack on prior semicolon
|
||||||
|
index = str.lastIndexOf(';', eqIdx - 1) + 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = str.slice(index, eqIdx).trim()
|
||||||
|
|
||||||
// only assign once
|
// only assign once
|
||||||
if (!__hasOwnProperty.call(obj, key)) {
|
if (undefined === obj[key]) {
|
||||||
var valStartIdx = startIndex(str, eqIdx + 1, endIdx);
|
var val = str.slice(eqIdx + 1, endIdx).trim()
|
||||||
var valEndIdx = endIndex(str, endIdx, valStartIdx);
|
|
||||||
|
|
||||||
if (str.charCodeAt(valStartIdx) === 0x22 /* " */ && str.charCodeAt(valEndIdx - 1) === 0x22 /* " */) {
|
// quoted values
|
||||||
valStartIdx++;
|
if (val.charCodeAt(0) === 0x22) {
|
||||||
valEndIdx--;
|
val = val.slice(1, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
var val = str.slice(valStartIdx, valEndIdx);
|
|
||||||
obj[key] = tryDecode(val, dec);
|
obj[key] = tryDecode(val, dec);
|
||||||
}
|
}
|
||||||
|
|
||||||
index = endIdx + 1
|
index = endIdx + 1
|
||||||
} while (index < len);
|
}
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
function startIndex(str, index, max) {
|
|
||||||
do {
|
|
||||||
var code = str.charCodeAt(index);
|
|
||||||
if (code !== 0x20 /* */ && code !== 0x09 /* \t */) return index;
|
|
||||||
} while (++index < max);
|
|
||||||
return max;
|
|
||||||
}
|
|
||||||
|
|
||||||
function endIndex(str, index, min) {
|
|
||||||
while (index > min) {
|
|
||||||
var code = str.charCodeAt(--index);
|
|
||||||
if (code !== 0x20 /* */ && code !== 0x09 /* \t */) return index + 1;
|
|
||||||
}
|
|
||||||
return min;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serialize data into a cookie header.
|
* Serialize data into a cookie header.
|
||||||
*
|
*
|
||||||
* Serialize a name value pair into a cookie string suitable for
|
* Serialize the a name value pair into a cookie string suitable for
|
||||||
* http headers. An optional options object specifies cookie parameters.
|
* http headers. An optional options object specified cookie parameters.
|
||||||
*
|
*
|
||||||
* serialize('foo', 'bar', { httpOnly: true })
|
* serialize('foo', 'bar', { httpOnly: true })
|
||||||
* => "foo=bar; httpOnly"
|
* => "foo=bar; httpOnly"
|
||||||
*
|
*
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @param {string} val
|
* @param {string} val
|
||||||
* @param {object} [opt]
|
* @param {object} [options]
|
||||||
* @return {string}
|
* @return {string}
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function serialize(name, val, opt) {
|
function serialize(name, val, options) {
|
||||||
var enc = (opt && opt.encode) || encodeURIComponent;
|
var opt = options || {};
|
||||||
|
var enc = opt.encode || encode;
|
||||||
|
|
||||||
if (typeof enc !== 'function') {
|
if (typeof enc !== 'function') {
|
||||||
throw new TypeError('option encode is invalid');
|
throw new TypeError('option encode is invalid');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cookieNameRegExp.test(name)) {
|
if (!fieldContentRegExp.test(name)) {
|
||||||
throw new TypeError('argument name is invalid');
|
throw new TypeError('argument name is invalid');
|
||||||
}
|
}
|
||||||
|
|
||||||
var value = enc(val);
|
var value = enc(val);
|
||||||
|
|
||||||
if (!cookieValueRegExp.test(value)) {
|
if (value && !fieldContentRegExp.test(value)) {
|
||||||
throw new TypeError('argument val is invalid');
|
throw new TypeError('argument val is invalid');
|
||||||
}
|
}
|
||||||
|
|
||||||
var str = name + '=' + value;
|
var str = name + '=' + value;
|
||||||
if (!opt) return str;
|
|
||||||
|
|
||||||
if (null != opt.maxAge) {
|
if (null != opt.maxAge) {
|
||||||
var maxAge = Math.floor(opt.maxAge);
|
var maxAge = opt.maxAge - 0;
|
||||||
|
|
||||||
if (!isFinite(maxAge)) {
|
if (isNaN(maxAge) || !isFinite(maxAge)) {
|
||||||
throw new TypeError('option maxAge is invalid')
|
throw new TypeError('option maxAge is invalid')
|
||||||
}
|
}
|
||||||
|
|
||||||
str += '; Max-Age=' + maxAge;
|
str += '; Max-Age=' + Math.floor(maxAge);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opt.domain) {
|
if (opt.domain) {
|
||||||
if (!domainValueRegExp.test(opt.domain)) {
|
if (!fieldContentRegExp.test(opt.domain)) {
|
||||||
throw new TypeError('option domain is invalid');
|
throw new TypeError('option domain is invalid');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,7 +147,7 @@ function serialize(name, val, opt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (opt.path) {
|
if (opt.path) {
|
||||||
if (!pathValueRegExp.test(opt.path)) {
|
if (!fieldContentRegExp.test(opt.path)) {
|
||||||
throw new TypeError('option path is invalid');
|
throw new TypeError('option path is invalid');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,7 +178,8 @@ function serialize(name, val, opt) {
|
|||||||
|
|
||||||
if (opt.priority) {
|
if (opt.priority) {
|
||||||
var priority = typeof opt.priority === 'string'
|
var priority = typeof opt.priority === 'string'
|
||||||
? opt.priority.toLowerCase() : opt.priority;
|
? opt.priority.toLowerCase()
|
||||||
|
: opt.priority
|
||||||
|
|
||||||
switch (priority) {
|
switch (priority) {
|
||||||
case 'low':
|
case 'low':
|
||||||
@ -307,6 +234,17 @@ function decode (str) {
|
|||||||
: str
|
: str
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL-encode value.
|
||||||
|
*
|
||||||
|
* @param {string} val
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function encode (val) {
|
||||||
|
return encodeURIComponent(val)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if value is a Date.
|
* Determine if value is a Date.
|
||||||
*
|
*
|
||||||
@ -315,7 +253,8 @@ function decode (str) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function isDate (val) {
|
function isDate (val) {
|
||||||
return __toString.call(val) === '[object Date]';
|
return __toString.call(val) === '[object Date]' ||
|
||||||
|
val instanceof Date
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
6
node_modules/cookie/package.json
generated
vendored
6
node_modules/cookie/package.json
generated
vendored
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "cookie",
|
"name": "cookie",
|
||||||
"description": "HTTP server cookie parsing and serialization",
|
"description": "HTTP server cookie parsing and serialization",
|
||||||
"version": "0.7.2",
|
"version": "0.6.0",
|
||||||
"author": "Roman Shtylman <shtylman@gmail.com>",
|
"author": "Roman Shtylman <shtylman@gmail.com>",
|
||||||
"contributors": [
|
"contributors": [
|
||||||
"Douglas Christopher Wilson <doug@somethingdoug.com>"
|
"Douglas Christopher Wilson <doug@somethingdoug.com>"
|
||||||
@ -29,7 +29,6 @@
|
|||||||
"SECURITY.md",
|
"SECURITY.md",
|
||||||
"index.js"
|
"index.js"
|
||||||
],
|
],
|
||||||
"main": "index.js",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
},
|
},
|
||||||
@ -39,6 +38,7 @@
|
|||||||
"test": "mocha --reporter spec --bail --check-leaks test/",
|
"test": "mocha --reporter spec --bail --check-leaks test/",
|
||||||
"test-ci": "nyc --reporter=lcov --reporter=text npm test",
|
"test-ci": "nyc --reporter=lcov --reporter=text npm test",
|
||||||
"test-cov": "nyc --reporter=html --reporter=text npm test",
|
"test-cov": "nyc --reporter=html --reporter=text npm test",
|
||||||
"update-bench": "node scripts/update-benchmark.js"
|
"update-bench": "node scripts/update-benchmark.js",
|
||||||
|
"version": "node scripts/version-history.js && git add HISTORY.md"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
package-lock.json
generated
24
package-lock.json
generated
@ -53,6 +53,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "1.0.4",
|
"cmdk": "1.0.4",
|
||||||
|
"cookie": "^0.6.0",
|
||||||
"cookies-next": "^5.1.0",
|
"cookies-next": "^5.1.0",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
@ -94,6 +95,7 @@
|
|||||||
"webdav": "^5.8.0"
|
"webdav": "^5.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/cookie": "^0.6.0",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/imapflow": "^1.0.20",
|
"@types/imapflow": "^1.0.20",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
@ -3313,6 +3315,13 @@
|
|||||||
"tslib": "^2.8.0"
|
"tslib": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/cookie": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/crypto-js": {
|
"node_modules/@types/crypto-js": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
|
||||||
@ -4041,9 +4050,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cookie": {
|
"node_modules/cookie": {
|
||||||
"version": "0.7.2",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
@ -5934,6 +5943,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/next-auth/node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/next-themes": {
|
"node_modules/next-themes": {
|
||||||
"version": "0.4.4",
|
"version": "0.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.4.tgz",
|
||||||
|
|||||||
@ -54,6 +54,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "1.0.4",
|
"cmdk": "1.0.4",
|
||||||
|
"cookie": "^0.6.0",
|
||||||
"cookies-next": "^5.1.0",
|
"cookies-next": "^5.1.0",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
@ -95,6 +96,7 @@
|
|||||||
"webdav": "^5.8.0"
|
"webdav": "^5.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/cookie": "^0.6.0",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/imapflow": "^1.0.20",
|
"@types/imapflow": "^1.0.20",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
|
|||||||
10
yarn.lock
10
yarn.lock
@ -1034,6 +1034,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.8.0"
|
tslib "^2.8.0"
|
||||||
|
|
||||||
|
"@types/cookie@^0.6.0":
|
||||||
|
version "0.6.0"
|
||||||
|
resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz"
|
||||||
|
integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==
|
||||||
|
|
||||||
"@types/crypto-js@^4.2.2":
|
"@types/crypto-js@^4.2.2":
|
||||||
version "4.2.2"
|
version "4.2.2"
|
||||||
resolved "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz"
|
resolved "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz"
|
||||||
@ -1495,6 +1500,11 @@ commander@^4.0.0:
|
|||||||
resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz"
|
resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz"
|
||||||
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
|
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
|
||||||
|
|
||||||
|
cookie@^0.6.0:
|
||||||
|
version "0.6.0"
|
||||||
|
resolved "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz"
|
||||||
|
integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
|
||||||
|
|
||||||
cookie@^0.7.0:
|
cookie@^0.7.0:
|
||||||
version "0.7.2"
|
version "0.7.2"
|
||||||
resolved "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz"
|
resolved "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user