cleaning
This commit is contained in:
parent
3b81ad058a
commit
a4412c081a
@ -15,13 +15,15 @@ const menuItems: Record<string, string> = {
|
|||||||
dossiers: process.env.NEXT_PUBLIC_IFRAME_DRIVE_URL || '',
|
dossiers: process.env.NEXT_PUBLIC_IFRAME_DRIVE_URL || '',
|
||||||
'the-message': process.env.NEXT_PUBLIC_IFRAME_THEMESSAGE_URL || '',
|
'the-message': process.env.NEXT_PUBLIC_IFRAME_THEMESSAGE_URL || '',
|
||||||
qg: process.env.NEXT_PUBLIC_IFRAME_MISSIONVIEW_URL || '',
|
qg: process.env.NEXT_PUBLIC_IFRAME_MISSIONVIEW_URL || '',
|
||||||
// Keep any existing custom ones
|
// Use environment variables for these items too
|
||||||
board: "https://example.com/board",
|
board: process.env.NEXT_PUBLIC_IFRAME_BOARD_URL || '',
|
||||||
chapter: "https://example.com/chapter",
|
chapter: process.env.NEXT_PUBLIC_IFRAME_CHAPTER_URL || '',
|
||||||
flow: "https://example.com/flow",
|
flow: process.env.NEXT_PUBLIC_IFRAME_FLOW_URL || '',
|
||||||
design: "https://example.com/design",
|
design: process.env.NEXT_PUBLIC_IFRAME_DESIGN_URL || '',
|
||||||
|
artlab: process.env.NEXT_PUBLIC_IFRAME_DESIGN_URL || '',
|
||||||
|
// External URLs
|
||||||
gitlab: "https://gitlab.com",
|
gitlab: "https://gitlab.com",
|
||||||
missions: "https://example.com/missions"
|
missions: process.env.NEXT_PUBLIC_IFRAME_MISSIONSBOARD_URL || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// Using a different approach for metadata that doesn't directly access params.section
|
// Using a different approach for metadata that doesn't directly access params.section
|
||||||
|
|||||||
@ -82,25 +82,13 @@ export const authOptions: NextAuthOptions = {
|
|||||||
},
|
},
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async jwt({ token, account, profile, user }: any) {
|
async jwt({ token, account, profile, user }: any) {
|
||||||
// Debug input parameters to understand what's available
|
|
||||||
console.log('JWT callback - input parameters:', {
|
|
||||||
hasAccount: !!account,
|
|
||||||
hasProfile: !!profile,
|
|
||||||
hasUser: !!user,
|
|
||||||
hasToken: !!token,
|
|
||||||
tokenSub: token?.sub,
|
|
||||||
tokenAccessToken: token?.accessToken ? '[exists]' : undefined
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initial sign in
|
// Initial sign in
|
||||||
if (account && account.access_token) {
|
if (account && account.access_token) {
|
||||||
console.log('JWT callback - NEW SIGN IN with access token detected');
|
|
||||||
token.accessToken = account.access_token;
|
token.accessToken = account.access_token;
|
||||||
token.refreshToken = account.refresh_token;
|
token.refreshToken = account.refresh_token;
|
||||||
|
|
||||||
// Process the raw profile data if available
|
// Process the raw profile data if available
|
||||||
if (user && user.raw_profile) {
|
if (user && user.raw_profile) {
|
||||||
console.log('JWT callback - Raw profile data found, extracting roles');
|
|
||||||
const rawProfile = user.raw_profile;
|
const rawProfile = user.raw_profile;
|
||||||
|
|
||||||
// Extract roles from all possible sources
|
// Extract roles from all possible sources
|
||||||
@ -108,7 +96,6 @@ export const authOptions: NextAuthOptions = {
|
|||||||
|
|
||||||
// Get roles from realm_access
|
// Get roles from realm_access
|
||||||
if (rawProfile.realm_access && Array.isArray(rawProfile.realm_access.roles)) {
|
if (rawProfile.realm_access && Array.isArray(rawProfile.realm_access.roles)) {
|
||||||
console.log('Found realm_access roles:', rawProfile.realm_access.roles);
|
|
||||||
roles = roles.concat(rawProfile.realm_access.roles);
|
roles = roles.concat(rawProfile.realm_access.roles);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,14 +105,12 @@ export const authOptions: NextAuthOptions = {
|
|||||||
if (clientId &&
|
if (clientId &&
|
||||||
rawProfile.resource_access[clientId] &&
|
rawProfile.resource_access[clientId] &&
|
||||||
Array.isArray(rawProfile.resource_access[clientId].roles)) {
|
Array.isArray(rawProfile.resource_access[clientId].roles)) {
|
||||||
console.log('Found client-specific roles:', rawProfile.resource_access[clientId].roles);
|
|
||||||
roles = roles.concat(rawProfile.resource_access[clientId].roles);
|
roles = roles.concat(rawProfile.resource_access[clientId].roles);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also check resource_access roles under 'account'
|
// Also check resource_access roles under 'account'
|
||||||
if (rawProfile.resource_access.account &&
|
if (rawProfile.resource_access.account &&
|
||||||
Array.isArray(rawProfile.resource_access.account.roles)) {
|
Array.isArray(rawProfile.resource_access.account.roles)) {
|
||||||
console.log('Found account roles:', rawProfile.resource_access.account.roles);
|
|
||||||
roles = roles.concat(rawProfile.resource_access.account.roles);
|
roles = roles.concat(rawProfile.resource_access.account.roles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -138,18 +123,13 @@ export const authOptions: NextAuthOptions = {
|
|||||||
// Always ensure user has basic user role
|
// Always ensure user has basic user role
|
||||||
const finalRoles = [...new Set([...cleanedRoles, 'user'])];
|
const finalRoles = [...new Set([...cleanedRoles, 'user'])];
|
||||||
|
|
||||||
// Add the application-specific roles for testing
|
// Map Keycloak roles to application roles
|
||||||
finalRoles.push('admin', 'dataintelligence', 'coding', 'expression', 'mediation');
|
token.role = mapToApplicationRoles(finalRoles);
|
||||||
|
|
||||||
// Store roles in token
|
|
||||||
token.role = [...new Set(finalRoles)]; // Ensure uniqueness
|
|
||||||
console.log('JWT callback - Extracted roles:', token.role);
|
|
||||||
} else if (user && user.role) {
|
} else if (user && user.role) {
|
||||||
console.log('JWT callback - Using roles from user object:', user.role);
|
|
||||||
token.role = Array.isArray(user.role) ? user.role : [user.role];
|
token.role = Array.isArray(user.role) ? user.role : [user.role];
|
||||||
} else {
|
} else {
|
||||||
console.log('JWT callback - No profile data, setting default roles');
|
// Default roles if no profile data available
|
||||||
token.role = ['user', 'admin', 'dataintelligence', 'coding', 'expression', 'mediation'];
|
token.role = ['user'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store user information
|
// Store user information
|
||||||
@ -159,30 +139,14 @@ export const authOptions: NextAuthOptions = {
|
|||||||
token.last_name = user.last_name || '';
|
token.last_name = user.last_name || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Token exists but no roles, add default roles for testing
|
// Token exists but no roles, add default user role
|
||||||
else if (token && !token.role) {
|
else if (token && !token.role) {
|
||||||
console.log('JWT callback - Token exists but no roles, adding defaults');
|
token.role = ['user'];
|
||||||
// For testing purposes, add all roles
|
|
||||||
token.role = ['user', 'admin', 'dataintelligence', 'coding', 'expression', 'mediation'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log the token roles
|
|
||||||
console.log('JWT token structure:', JSON.stringify({
|
|
||||||
sub: token.sub,
|
|
||||||
role: token.role,
|
|
||||||
username: token.username
|
|
||||||
}, null, 2));
|
|
||||||
console.log('JWT token roles:', token.role);
|
|
||||||
return token;
|
return token;
|
||||||
},
|
},
|
||||||
async session({ session, token }: any) {
|
async session({ session, token }: any) {
|
||||||
console.log('Session callback - input parameters:', {
|
|
||||||
hasSession: !!session,
|
|
||||||
hasToken: !!token,
|
|
||||||
tokenRole: token?.role,
|
|
||||||
tokenSub: token?.sub
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pass necessary info to the session
|
// Pass necessary info to the session
|
||||||
session.accessToken = token.accessToken;
|
session.accessToken = token.accessToken;
|
||||||
if (session.user) {
|
if (session.user) {
|
||||||
@ -194,18 +158,13 @@ export const authOptions: NextAuthOptions = {
|
|||||||
session.user.username = token.username || '';
|
session.user.username = token.username || '';
|
||||||
session.user.first_name = token.first_name || '';
|
session.user.first_name = token.first_name || '';
|
||||||
session.user.last_name = token.last_name || '';
|
session.user.last_name = token.last_name || '';
|
||||||
console.log('Session callback - using token roles:', token.role);
|
|
||||||
} else {
|
} else {
|
||||||
// Fallback roles - ENSURE ALL REQUIRED ROLES ARE INCLUDED
|
// Fallback roles
|
||||||
session.user.role = ["user", "admin", "dataintelligence", "coding", "expression", "mediation"];
|
session.user.role = ["user"];
|
||||||
session.user.username = '';
|
session.user.username = '';
|
||||||
session.user.first_name = '';
|
session.user.first_name = '';
|
||||||
session.user.last_name = '';
|
session.user.last_name = '';
|
||||||
console.log('Session callback - no token roles, using fallback');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log the session user roles
|
|
||||||
console.log('Session user roles:', session.user.role);
|
|
||||||
}
|
}
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
@ -225,7 +184,7 @@ export const authOptions: NextAuthOptions = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
debug: true, // Enable debug logs to help with troubleshooting
|
debug: process.env.NODE_ENV === 'development',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -249,40 +208,22 @@ function mapToApplicationRoles(keycloakRoles: string[]): string[] {
|
|||||||
'offline_access': ['user'],
|
'offline_access': ['user'],
|
||||||
// Add more mappings as needed
|
// Add more mappings as needed
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Map each role and flatten the result
|
||||||
|
let appRoles: string[] = ['user']; // Always include 'user' role
|
||||||
|
|
||||||
// Convert all keycloak roles to lowercase for case-insensitive matching
|
for (const role of keycloakRoles) {
|
||||||
const lowerKeycloakRoles = keycloakRoles.map(role => role.toLowerCase());
|
const mappedRoles = mappings[role.toLowerCase()];
|
||||||
|
if (mappedRoles) {
|
||||||
// Map roles based on the defined mappings
|
appRoles = [...appRoles, ...mappedRoles];
|
||||||
let applicationRoles: string[] = [];
|
|
||||||
|
|
||||||
// Check all Keycloak roles for matches in our mapping
|
|
||||||
for (const role of lowerKeycloakRoles) {
|
|
||||||
if (mappings[role]) {
|
|
||||||
applicationRoles = applicationRoles.concat(mappings[role]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle any role that contains certain keywords
|
|
||||||
if (role.includes('admin')) {
|
|
||||||
applicationRoles.push('admin', 'dataintelligence', 'coding', 'expression', 'mediation');
|
|
||||||
} else if (role.includes('developer') || role.includes('dev')) {
|
|
||||||
applicationRoles.push('coding', 'dataintelligence');
|
|
||||||
} else if (role.includes('design')) {
|
|
||||||
applicationRoles.push('expression');
|
|
||||||
} else if (role.includes('data')) {
|
|
||||||
applicationRoles.push('dataintelligence');
|
|
||||||
} else if (role.includes('mediat')) {
|
|
||||||
applicationRoles.push('mediation');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure user always has basic access
|
// Remove duplicates and return
|
||||||
applicationRoles.push('user');
|
return [...new Set(appRoles)];
|
||||||
|
|
||||||
// Return unique application roles
|
|
||||||
return [...new Set(applicationRoles)];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handler = NextAuth(authOptions);
|
const handler = NextAuth(authOptions);
|
||||||
|
|
||||||
export { handler as GET, handler as POST };
|
export { handler as GET, handler as POST };
|
||||||
|
|
||||||
|
|||||||
@ -10,14 +10,14 @@ export default async function ArtlabPage() {
|
|||||||
redirect("/signin");
|
redirect("/signin");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the direct URL from environment variable
|
// Get the design URL from environment variable - intentionally shares URL with design section
|
||||||
const iframeUrl = process.env.NEXT_PUBLIC_IFRAME_DESIGN_URL || '';
|
const designIframeUrl = process.env.NEXT_PUBLIC_IFRAME_DESIGN_URL || '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="w-full h-screen bg-black">
|
<main className="w-full h-screen bg-black">
|
||||||
<div className="w-full h-full px-4 pt-12 pb-4">
|
<div className="w-full h-full px-4 pt-12 pb-4">
|
||||||
<ResponsiveIframe
|
<ResponsiveIframe
|
||||||
src={iframeUrl}
|
src={designIframeUrl}
|
||||||
allowFullScreen={true}
|
allowFullScreen={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -27,7 +27,6 @@ const SERVICE_URLS: Record<string, string> = {
|
|||||||
'the-message': process.env.NEXT_PUBLIC_IFRAME_THEMESSAGE_URL || '',
|
'the-message': process.env.NEXT_PUBLIC_IFRAME_THEMESSAGE_URL || '',
|
||||||
'qg': process.env.NEXT_PUBLIC_IFRAME_MISSIONVIEW_URL || '',
|
'qg': process.env.NEXT_PUBLIC_IFRAME_MISSIONVIEW_URL || '',
|
||||||
'design': process.env.NEXT_PUBLIC_IFRAME_DESIGN_URL || '',
|
'design': process.env.NEXT_PUBLIC_IFRAME_DESIGN_URL || '',
|
||||||
'artlab': process.env.NEXT_PUBLIC_IFRAME_DESIGN_URL || ''
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ResponsiveIframe({
|
export default function ResponsiveIframe({
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
|
|
||||||
export function Todo() {
|
|
||||||
const todos = [
|
|
||||||
{ text: "send e-mails", done: false },
|
|
||||||
{ text: "do the visuals", done: false },
|
|
||||||
{ text: "write the contract", done: false },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className='transition-transform duration-500 ease-in-out transform hover:scale-105'>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Todo</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className='space-y-4'>
|
|
||||||
{todos.map((todo, i) => (
|
|
||||||
<div key={i} className='flex items-center space-x-2'>
|
|
||||||
<Checkbox id={`todo-${i}`} />
|
|
||||||
<label
|
|
||||||
htmlFor={`todo-${i}`}
|
|
||||||
className='text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
|
|
||||||
>
|
|
||||||
{todo.text}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<Button className='w-full mt-4' variant='secondary'>
|
|
||||||
Add Todo
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -473,7 +473,17 @@ export const useEmailState = () => {
|
|||||||
const newFlaggedStatus = !email.flags.flagged;
|
const newFlaggedStatus = !email.flags.flagged;
|
||||||
logEmailOp('TOGGLE_STAR', `Setting starred status to ${newFlaggedStatus} for email ${emailId}`);
|
logEmailOp('TOGGLE_STAR', `Setting starred status to ${newFlaggedStatus} for email ${emailId}`);
|
||||||
|
|
||||||
// TODO: Implement optimistic update
|
// Implement optimistic update
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_EMAIL_FLAGS',
|
||||||
|
payload: {
|
||||||
|
emailId,
|
||||||
|
flags: {
|
||||||
|
flagged: newFlaggedStatus
|
||||||
|
},
|
||||||
|
accountId: email.accountId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Make API call
|
// Make API call
|
||||||
const response = await fetch(`/api/courrier/${emailId}/flag`, {
|
const response = await fetch(`/api/courrier/${emailId}/flag`, {
|
||||||
|
|||||||
@ -52,6 +52,7 @@ export type EmailAction =
|
|||||||
| { type: 'UPDATE_UNREAD_COUNT', payload: { accountId: string, folder: string, count: number } }
|
| { type: 'UPDATE_UNREAD_COUNT', payload: { accountId: string, folder: string, count: number } }
|
||||||
| { type: 'SET_UNREAD_COUNTS', payload: Record<string, Record<string, number>> }
|
| { type: 'SET_UNREAD_COUNTS', payload: Record<string, Record<string, number>> }
|
||||||
| { type: 'TOGGLE_SHOW_FOLDERS', payload: boolean }
|
| { type: 'TOGGLE_SHOW_FOLDERS', payload: boolean }
|
||||||
|
| { type: 'UPDATE_EMAIL_FLAGS', payload: { emailId: string, flags: { flagged: boolean }, accountId?: string } }
|
||||||
| { type: 'MARK_EMAIL_AS_READ', payload: { emailId: string, isRead: boolean, accountId?: string } };
|
| { type: 'MARK_EMAIL_AS_READ', payload: { emailId: string, isRead: boolean, accountId?: string } };
|
||||||
|
|
||||||
// Initial state
|
// Initial state
|
||||||
@ -420,6 +421,30 @@ export function emailReducer(state: EmailState, action: EmailAction): EmailState
|
|||||||
showFolders: action.payload
|
showFolders: action.payload
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case 'UPDATE_EMAIL_FLAGS': {
|
||||||
|
const { emailId, flags, accountId } = action.payload;
|
||||||
|
|
||||||
|
// Update emails list
|
||||||
|
const updatedEmails = state.emails.map(email =>
|
||||||
|
(email.id === emailId && (!accountId || email.accountId === accountId))
|
||||||
|
? { ...email, flags: { ...email.flags, ...flags } }
|
||||||
|
: email
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update selected email if it matches
|
||||||
|
const updatedSelectedEmail = state.selectedEmail &&
|
||||||
|
state.selectedEmail.id === emailId &&
|
||||||
|
(!accountId || state.selectedEmail.accountId === accountId)
|
||||||
|
? { ...state.selectedEmail, flags: { ...state.selectedEmail.flags, ...flags } }
|
||||||
|
: state.selectedEmail;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
emails: updatedEmails,
|
||||||
|
selectedEmail: updatedSelectedEmail
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
case 'MARK_EMAIL_AS_READ': {
|
case 'MARK_EMAIL_AS_READ': {
|
||||||
const { emailId, isRead, accountId } = action.payload;
|
const { emailId, isRead, accountId } = action.payload;
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,33 @@ const nextConfig = {
|
|||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
|
eslint: {
|
||||||
|
ignoreDuringBuilds: true,
|
||||||
|
},
|
||||||
|
typescript: {
|
||||||
|
ignoreBuildErrors: true,
|
||||||
|
},
|
||||||
|
images: {
|
||||||
|
unoptimized: true,
|
||||||
|
},
|
||||||
|
experimental: {
|
||||||
|
webpackBuildWorker: true,
|
||||||
|
parallelServerBuildTraces: true,
|
||||||
|
parallelServerCompiles: true,
|
||||||
|
},
|
||||||
|
async headers() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/:path*',
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: 'Content-Security-Policy',
|
||||||
|
value: "frame-ancestors 'self' https://espace.slm-lab.net https://connect.slm-lab.net"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = nextConfig;
|
module.exports = nextConfig;
|
||||||
@ -1,61 +0,0 @@
|
|||||||
let userConfig;
|
|
||||||
try {
|
|
||||||
userConfig = await import("./v0-user-next.config");
|
|
||||||
} catch (e) {
|
|
||||||
// ignore error
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
|
||||||
const nextConfig = {
|
|
||||||
eslint: {
|
|
||||||
ignoreDuringBuilds: true,
|
|
||||||
},
|
|
||||||
typescript: {
|
|
||||||
ignoreBuildErrors: true,
|
|
||||||
},
|
|
||||||
images: {
|
|
||||||
unoptimized: true,
|
|
||||||
},
|
|
||||||
experimental: {
|
|
||||||
webpackBuildWorker: true,
|
|
||||||
parallelServerBuildTraces: true,
|
|
||||||
parallelServerCompiles: true,
|
|
||||||
},
|
|
||||||
async headers() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
source: '/:path*',
|
|
||||||
headers: [
|
|
||||||
{
|
|
||||||
key: 'Content-Security-Policy',
|
|
||||||
value: "frame-ancestors 'self' https://espace.slm-lab.net https://connect.slm-lab.net"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
mergeConfig(nextConfig, userConfig);
|
|
||||||
|
|
||||||
function mergeConfig(nextConfig, userConfig) {
|
|
||||||
if (!userConfig) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key in userConfig) {
|
|
||||||
if (
|
|
||||||
typeof nextConfig[key] === "object" &&
|
|
||||||
!Array.isArray(nextConfig[key])
|
|
||||||
) {
|
|
||||||
nextConfig[key] = {
|
|
||||||
...nextConfig[key],
|
|
||||||
...userConfig[key],
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
nextConfig[key] = userConfig[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default nextConfig;
|
|
||||||
Loading…
Reference in New Issue
Block a user