Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
lts/jod
18.17.0
16 changes: 16 additions & 0 deletions dev.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash

# Auto-switch to correct Node.js version and start dev server
echo "🚀 Starting Tableau Embedding Playbook Development Server"
echo "========================================================"

# Check if nvm is available
if command -v nvm &> /dev/null; then
echo "📦 Switching to Node.js version specified in .nvmrc..."
nvm use
else
echo "⚠️ nvm not found. Please ensure you're using Node.js 18.17.0+"
fi

echo "🔧 Starting development server on port 3001..."
npm run dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDrWpY2DG
LS76oMng5F90DBAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIKTlmV/tcXfrlxWG
UH+xdR0uofwQNjJczPEoc1hr9GHPAAAAoMgND3S4JelppliKkIVPGxVmdYm0BsLZofP+NT
W5i4Arv/y741ji++C8m55aKepwXP86Q+pvMOVq4PkLwsDce+1WhGlbOrzi6PXSLjjguwDp
CmPLzuqVmKbLodAdFAWPAvGD7EekpshoX2WcmHNTyHuoSKCVvzH6OFkX8jtepbPyECU0Qp
o/X+YzhZ2UJ/g7WEk3oFqm4ax0QV8uQx6FbS8=
-----END OPENSSH PRIVATE KEY-----
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKTlmV/tcXfrlxWGUH+xdR0uofwQNjJczPEoc1hr9GHP allisonreynoldsc@gmail.com
9 changes: 9 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ const withNextra = nextra({
})

export default withNextra({
// Performance optimizations
swcMinify: true,
compress: true,
poweredByHeader: false,
generateEtags: false,

images: {
remotePatterns: [
{
Expand All @@ -23,6 +29,9 @@ export default withNextra({
pathname: '/**',
}
],
// Optimize images
formats: ['image/webp', 'image/avif'],
minimumCacheTTL: 60,
},
webpack(config) {
// config.optimization.minimize = false;
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
"version": "0.0.1",
"description": "Tableau Embedded Playbook",
"scripts": {
"dev": "next lint && next dev",
"dev": "next dev --port 3000",
"dev:fast": "next dev --port 3000 --turbo",
"dev:lint": "next lint && next dev --port 3000",
"build": "next build",
"export": "next export",
"start": "next start",
"lint": "next lint",
"lint:fix": "next lint --fix",
"info": "next info",
"demo": "next lint && next build && next start",
"save": "git add pages public && git commit -m 'saving content (/public & /pages)'",
Expand Down
Binary file added public/img/demos/servicedesk.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/themes/servicedesk/dataicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
102 changes: 102 additions & 0 deletions src/app/api/auth/[...nextauth]/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,28 +40,41 @@ export const authOptions: AuthOptions = {
demo: { label: "Demo", type: "text" }
},
async authorize(credentials: any, req) {
console.log('🔐 NextAuth Authorize Debug:');
console.log('Credentials:', credentials);

let user: any = null;

const demoManager = new UserModel();
const currentDemo = demoManager.getDemoByName(credentials.demo);
console.log('Current Demo:', currentDemo);

if (currentDemo) {
// Find the user in the users array of the matched demo object
const matchedUser = currentDemo.users.find(
(user) => user.id.toUpperCase() === credentials.ID.toUpperCase()
);
console.log('Matched User:', matchedUser);

if (matchedUser) {
user = { ...matchedUser }; // Clone the matched user object
user.demo = credentials.demo;
user.uaf = user.uaf || {};
console.log('User UAF:', user.uaf);

const jwt_client_id = process.env.TABLEAU_JWT_CLIENT_ID;
const embed_secret = process.env.TABLEAU_EMBED_JWT_SECRET;
const embed_secret_id = process.env.TABLEAU_EMBED_JWT_SECRET_ID;
const rest_secret = process.env.TABLEAU_REST_JWT_SECRET;
const rest_secret_id = process.env.TABLEAU_REST_JWT_SECRET_ID;

console.log('JWT Environment Variables:');
console.log('JWT Client ID:', jwt_client_id ? 'SET' : 'NOT SET');
console.log('Embed Secret:', embed_secret ? 'SET' : 'NOT SET');
console.log('Embed Secret ID:', embed_secret_id ? 'SET' : 'NOT SET');
console.log('Rest Secret:', rest_secret ? 'SET' : 'NOT SET');
console.log('Rest Secret ID:', rest_secret_id ? 'SET' : 'NOT SET');

// Client-safe Connected App scopes
const embed_scopes = [
"tableau:views:embed",
Expand Down Expand Up @@ -91,8 +104,10 @@ export const authOptions: AuthOptions = {
jwt_client_id
};

console.log('🚀 Creating Tableau session...');
const session = new SessionModel(user.name);
await session.jwt(user.email, embed_options, embed_scopes, rest_options, rest_scopes, user.uaf);
console.log('Session Authorized:', session.authorized);

if (session.authorized) {
const {
Expand All @@ -118,6 +133,9 @@ export const authOptions: AuthOptions = {
created,
expires
};
console.log('✅ User tableau data set:', user.tableau);
} else {
console.error('❌ Session not authorized!');
}

return user.tableau ? user : null;
Expand Down Expand Up @@ -160,13 +178,97 @@ export const authOptions: AuthOptions = {
token.uaf = user.uaf || {};
token.tableau = user.tableau;
token.rest_token = user.rest_token;
token.token_created = Date.now();
}

// Check if tokens need refresh (JWT tokens expire in 9 minutes)
if (token.tableau && token.token_created) {
const tokenAge = Date.now() - (token.token_created as number);
const refreshThreshold = 8 * 60 * 1000; // 8 minutes in milliseconds

if (tokenAge > refreshThreshold) {
console.log('🔄 JWT tokens are old, refreshing...');
try {
// Import the refresh function here to avoid circular dependencies
const { handleJWT } = await import('@/models/Session/controller');

const jwt_client_id = process.env.TABLEAU_JWT_CLIENT_ID;
const embed_secret = process.env.TABLEAU_EMBED_JWT_SECRET;
const embed_secret_id = process.env.TABLEAU_EMBED_JWT_SECRET_ID;
const rest_secret = process.env.TABLEAU_REST_JWT_SECRET;
const rest_secret_id = process.env.TABLEAU_REST_JWT_SECRET_ID;

const embed_scopes = [
"tableau:views:embed",
"tableau:views:embed_authoring",
"tableau:insights:embed",
];
const embed_options = {
jwt_secret: embed_secret,
jwt_secret_id: embed_secret_id,
jwt_client_id
};

const rest_scopes = [
"tableau:content:read",
"tableau:datasources:read",
"tableau:workbooks:read",
"tableau:projects:read",
"tableau:insights:read",
"tableau:metric_subscriptions:read",
"tableau:insight_definitions_metrics:read",
"tableau:insight_metrics:read",
"tableau:metrics:download",
];
const rest_options = {
jwt_secret: rest_secret,
jwt_secret_id: rest_secret_id,
jwt_client_id
};

const { credentials, rest_token, embed_token } = await handleJWT(
token.email as string,
embed_options,
embed_scopes,
rest_options,
rest_scopes,
(token.uaf as any) || {}
);

// Update token with fresh data
token.tableau = {
...token.tableau,
username: credentials.username,
user_id: credentials.user_id,
embed_token,
rest_token,
rest_key: credentials.rest_key,
site_id: credentials.site_id,
site: credentials.site,
created: credentials.created,
expires: credentials.expiration
};
token.rest_token = rest_token;
token.token_created = Date.now();

console.log('✅ JWT tokens refreshed successfully');
} catch (error) {
console.error('❌ Failed to refresh JWT tokens:', error);
// Don't throw error, just log it and continue with existing tokens
}
}
}

return token;
},
async session({ session, token }: { session: Session; token: JWT; }) {
const customSession = session as CustomSession;
if (customSession.user) {
customSession.user.demo = token.demo as string;
// Add tableau data to session for client-side access
(customSession as any).tableau = token.tableau;
(customSession as any).rest_token = token.rest_token;
(customSession as any).embed_token = (token.tableau as any)?.embed_token;
}
return session;
}
Expand Down
124 changes: 124 additions & 0 deletions src/app/api/auth/refresh-tokens/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { NextRequest, NextResponse } from 'next/server';
import { getToken } from 'next-auth/jwt';
import { SessionModel } from '@/models';
import { UAF } from '@/models/Session/controller';

interface TableauToken {
username?: string;
user_id?: string;
embed_token?: string;
rest_token?: string;
rest_key?: string;
site_id?: string;
site?: string;
created?: Date;
expires?: Date;
uaf?: UAF;
}

export async function POST(request: NextRequest) {
try {
// Get the current JWT token from the session
const token = await getToken({ req: request });

if (!token?.tableau) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}

const tableau = token.tableau as TableauToken;
const userEmail = token.email;

if (!userEmail) {
return NextResponse.json({ error: 'User email not found' }, { status: 400 });
}

console.log('🔄 Refreshing tokens for user:', userEmail);

// Get the JWT configuration from environment variables
const jwt_client_id = process.env.TABLEAU_JWT_CLIENT_ID;
const embed_secret = process.env.TABLEAU_EMBED_JWT_SECRET;
const embed_secret_id = process.env.TABLEAU_EMBED_JWT_SECRET_ID;
const rest_secret = process.env.TABLEAU_REST_JWT_SECRET;
const rest_secret_id = process.env.TABLEAU_REST_JWT_SECRET_ID;

if (!jwt_client_id || !embed_secret || !embed_secret_id || !rest_secret || !rest_secret_id) {
return NextResponse.json({ error: 'JWT configuration missing' }, { status: 500 });
}

// Client-safe Connected App scopes
const embed_scopes = [
"tableau:views:embed",
"tableau:views:embed_authoring",
"tableau:insights:embed",
];
const embed_options = {
jwt_secret: embed_secret,
jwt_secret_id: embed_secret_id,
jwt_client_id
};

// Backend secured Connected App scopes
const rest_scopes = [
"tableau:content:read",
"tableau:datasources:read",
"tableau:workbooks:read",
"tableau:projects:read",
"tableau:insights:read",
"tableau:metric_subscriptions:read",
"tableau:insight_definitions_metrics:read",
"tableau:insight_metrics:read",
"tableau:metrics:download",
];
const rest_options = {
jwt_secret: rest_secret,
jwt_secret_id: rest_secret_id,
jwt_client_id
};

// Create a new session with fresh tokens
const session = new SessionModel(userEmail);
await session.jwt(userEmail, embed_options, embed_scopes, rest_options, rest_scopes, tableau.uaf || {});

if (!session.authorized) {
return NextResponse.json({ error: 'Failed to refresh tokens' }, { status: 500 });
}

const {
username,
user_id,
embed_token,
rest_token,
rest_key,
site_id,
site,
created,
expires
} = session;

// Return the refreshed token data
const refreshedTokens = {
tableau: {
username,
user_id,
embed_token,
rest_token,
rest_key,
site_id,
site,
created,
expires
}
};

console.log('✅ Tokens refreshed successfully for user:', userEmail);

return NextResponse.json(refreshedTokens);

} catch (error) {
console.error('❌ Error refreshing tokens:', error);
return NextResponse.json({
error: 'Failed to refresh tokens',
details: error.message
}, { status: 500 });
}
}
Loading