Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
type="image/x-icon"
href="https://s3.us-east-1.amazonaws.com/assets.pythagora.ai/logos/favicon.ico"
/>
<link rel="icon" type="image/x-icon" href="https://s3.us-east-1.amazonaws.com/assets.pythagora.ai/logos/favicon.ico" />
</head>
<body>
<div id="root"></div>
Expand Down
15 changes: 8 additions & 7 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,42 +13,43 @@
"dependencies": {
"@hookform/resolvers": "^3.9.1",
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-aspect-ratio": "^1.1.0",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-collapsible": "^1.1.1",
"@radix-ui/react-context-menu": "^2.2.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-hover-card": "^1.1.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-menubar": "^1.1.2",
"@radix-ui/react-navigation-menu": "^1.2.1",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.2.1",
"@radix-ui/react-scroll-area": "^1.2.1",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.2.1",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-toast": "^1.2.2",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4",
"@stripe/react-stripe-js": "^3.7.0",
"@stripe/stripe-js": "^7.3.0",
"axios": "^1.7.8",
"@stripe/stripe-js": "^7.3.1",
"axios": "^1.10.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.5.1",
"input-otp": "^1.4.1",
"json-bigint": "^1.0.0",
"lottie-react": "^2.4.1",
"lucide-react": "^0.460.0",
"next-themes": "^0.4.3",
"react": "^18.3.1",
Expand Down
9 changes: 9 additions & 0 deletions client/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,12 @@
.read-the-docs {
color: #888;
}

::-webkit-scrollbar {
display: none;
}

html, body {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
23 changes: 11 additions & 12 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Toaster } from "./components/ui/toaster";
import { AuthProvider } from "./contexts/AuthContext";
import { Login } from "./pages/Login";
import { Register } from "./pages/Register";
import { AcceptInvitePage } from "./pages/AcceptInvitePage";
import { ProtectedRoute } from "./components/ProtectedRoute";
import { DashboardLayout } from "./components/DashboardLayout";
import { AccountPage } from "./pages/AccountPage";
Expand All @@ -27,6 +28,14 @@ function App() {
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route
path="/accept-invite"
element={
<ProtectedRoute>
<AcceptInvitePage />
</ProtectedRoute>
}
/>
<Route
path="/"
element={
Expand All @@ -40,17 +49,7 @@ function App() {
<Route path="payments" element={<PaymentsPage />} />
<Route path="domains" element={<DomainsPage />} />
<Route path="settings" element={<SettingsPage />} />
<Route path="projects">
<Route
index
element={<Navigate to="/projects/drafts" replace />}
/>
<Route path="drafts" element={<ProjectsPage type="drafts" />} />
<Route
path="deployed"
element={<ProjectsPage type="deployed" />}
/>
</Route>
<Route path="projects" element={<ProjectsPage />} />
<Route path="team" element={<TeamPage />} />
</Route>
</Routes>
Expand All @@ -61,4 +60,4 @@ function App() {
);
}

export default App;
export default App;
129 changes: 93 additions & 36 deletions client/src/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import axios, { AxiosRequestConfig, AxiosError } from "axios";
import JSONbig from "json-bigint";
import { PYTHAGORA_API_URL, DEPLOYMENT_URL } from "../constants/api";

const localApi = axios.create({
const pythagoraApi = axios.create({
baseURL: PYTHAGORA_API_URL,
headers: {
"Content-Type": "application/json",
},
Expand All @@ -11,33 +13,68 @@ const localApi = axios.create({
transformResponse: [(data) => JSONbig.parse(data)],
});

let accessToken: string | null = null;
// Initialize accessToken immediately from localStorage
let accessToken: string | null = localStorage.getItem("accessToken");
console.log("API: Initial access token from localStorage:", !!accessToken);

const getApiInstance = (url: string) => {
console.log("Getting API instance for URL:", url);
console.log("Current access token exists:", !!accessToken);
return localApi;
// Helper function to get all cookie names for debugging
const getAllCookieNames = (): string[] => {
const cookies = document.cookie.split(';');
const cookieNames = cookies.map(cookie => {
const name = cookie.trim().split('=')[0];
return name;
}).filter(name => name.length > 0);

console.log("API DEBUG: All available cookie names:", cookieNames);
return cookieNames;
};

// Helper function to get cookie value
const getCookie = (name: string): string | null => {
console.log("API getCookie: Looking for cookie:", name);
console.log("API getCookie: All cookies:", document.cookie);

// Debug: Show all available cookie names
getAllCookieNames();

const value = `; ${document.cookie}`;
console.log("API getCookie: Formatted cookie string:", value);

const parts = value.split(`; ${name}=`);
console.log("API getCookie: Split parts:", parts);
console.log("API getCookie: Parts length:", parts.length);

if (parts.length === 2) {
const result = parts.pop()?.split(';').shift() || null;
console.log("API getCookie: Extracted value:", result);
return result;
}

console.log("API getCookie: Cookie not found");
return null;
};

const isAuthEndpoint = (url: string): boolean => {
return url.includes("/api/auth");
// Helper function to delete cookie
const deleteCookie = (name: string) => {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
};

const setupInterceptors = (apiInstance: typeof axios) => {
apiInstance.interceptors.request.use(
(config: AxiosRequestConfig): AxiosRequestConfig => {
console.log("Making request to:", config.url);
console.log("API: Making request to Pythagora API:", config.url);

// Always ensure we have the latest token from localStorage
if (!accessToken) {
accessToken = localStorage.getItem("accessToken");
console.log("Retrieved token from localStorage:", !!accessToken);
console.log("API: Retrieved token from localStorage:", !!accessToken);
}

if (accessToken && config.headers) {
config.headers.Authorization = `Bearer ${accessToken}`;
console.log("Added Authorization header");
console.log("API: Added Authorization header");
} else {
console.log("No token available for request");
console.log("API: No token available for request");
}

return config;
Expand All @@ -48,6 +85,8 @@ const setupInterceptors = (apiInstance: typeof axios) => {
apiInstance.interceptors.response.use(
(response) => response,
async (error: AxiosError): Promise<any> => {
console.log("API: Response error interceptor triggered", error.response?.status);

const originalRequest = error.config as AxiosRequestConfig & {
_retry?: boolean;
};
Expand All @@ -56,27 +95,50 @@ const setupInterceptors = (apiInstance: typeof axios) => {
[401, 403].includes(error.response?.status) &&
!originalRequest._retry
) {
console.log("API: Attempting token refresh due to 401/403 error");
originalRequest._retry = true;

try {
if (isAuthEndpoint(originalRequest.url || "")) {
const { data } = await localApi.post(`/api/auth/refresh`, {
refreshToken: localStorage.getItem("refreshToken"),
});
accessToken = data.data.accessToken;
localStorage.setItem("accessToken", accessToken);
localStorage.setItem("refreshToken", data.data.refreshToken);
console.log("API: Making refresh token request to Pythagora API with httpOnly cookie");
const refreshResponse = await fetch(`${PYTHAGORA_API_URL}/auth/refresh-token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // This will include httpOnly cookies
body: JSON.stringify({}), // Empty payload as requested
});

console.log("API: Refresh response status:", refreshResponse.status);

if (!refreshResponse.ok) {
console.log("API: Refresh response not ok, status:", refreshResponse.status);
const errorText = await refreshResponse.text();
console.log("API: Refresh error response:", errorText);
throw new Error("Token refresh failed");
}

if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
const data = await refreshResponse.json();
console.log("API: Refresh response data received:", !!data.accessToken);

if (data.accessToken) {
accessToken = data.accessToken;
localStorage.setItem("accessToken", accessToken);
console.log("API: New access token stored, retrying original request");

if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
}
return pythagoraApi(originalRequest);
} else {
console.log("API: No access token in refresh response");
throw new Error("No access token in refresh response");
}
return getApiInstance(originalRequest.url || "")(originalRequest);
} catch (err) {
localStorage.removeItem("refreshToken");
console.log("API: Token refresh failed, cleaning up and redirecting");
localStorage.removeItem("accessToken");
accessToken = null;
window.location.href = "/login";
window.location.href = `https://pythagora.ai/log-in?return_to=${DEPLOYMENT_URL}`;
return Promise.reject(err);
}
}
Expand All @@ -86,29 +148,24 @@ const setupInterceptors = (apiInstance: typeof axios) => {
);
};

setupInterceptors(localApi);
setupInterceptors(pythagoraApi);

const api = {
request: (config: AxiosRequestConfig) => {
const apiInstance = getApiInstance(config.url || "");
return apiInstance(config);
return pythagoraApi(config);
},
get: (url: string, config?: AxiosRequestConfig) => {
const apiInstance = getApiInstance(url);
return apiInstance.get(url, config);
return pythagoraApi.get(url, config);
},
post: (url: string, data?: any, config?: AxiosRequestConfig) => {
const apiInstance = getApiInstance(url);
return apiInstance.post(url, data, config);
return pythagoraApi.post(url, data, config);
},
put: (url: string, data?: any, config?: AxiosRequestConfig) => {
const apiInstance = getApiInstance(url);
return apiInstance.put(url, data, config);
return pythagoraApi.put(url, data, config);
},
delete: (url: string, config?: AxiosRequestConfig) => {
const apiInstance = getApiInstance(url);
return apiInstance.delete(url, config);
return pythagoraApi.delete(url, config);
},
};

export default api;
export default api;
40 changes: 33 additions & 7 deletions client/src/api/auth.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import api from "./api";

// Description: Login user functionality
// Endpoint: POST /api/auth/login
// Endpoint: POST /auth/login
// Request: { email: string, password: string }
// Response: { accessToken: string, refreshToken: string }
export const login = async (email: string, password: string) => {
try {
const response = await api.post("/api/auth/login", { email, password });
const response = await api.post("/auth/login", { email, password });
return response.data;
} catch (error) {
console.error("Login error:", error);
Expand All @@ -15,7 +15,7 @@ export const login = async (email: string, password: string) => {
};

// Description: Register user functionality
// Endpoint: POST /api/auth/register
// Endpoint: POST /auth/register
// Request: { name: string, email: string, password: string }
// Response: { email: string, accessToken: string }
export const register = async (
Expand All @@ -24,7 +24,7 @@ export const register = async (
password: string,
) => {
try {
const response = await api.post("/api/auth/register", {
const response = await api.post("/auth/register", {
name,
email,
password,
Expand All @@ -35,14 +35,40 @@ export const register = async (
}
};

// Description: Refresh token functionality
// Endpoint: POST /auth/refresh-token
// Request: { refreshToken: string }
// Response: { success: boolean, data: { accessToken: string, refreshToken: string } }
export const refreshToken = async (refreshToken: string) => {
try {
const response = await api.post("/auth/refresh-token", { refreshToken });
return response.data;
} catch (error) {
throw new Error(error?.response?.data?.message || error.message);
}
};

// Description: Get current user info
// Endpoint: GET /auth/me
// Request: {}
// Response: { userId: string, email: string, name: string, subscription: object }
export const getCurrentUser = async () => {
try {
const response = await api.get("/auth/me");
return response.data;
} catch (error) {
throw new Error(error?.response?.data?.message || error.message);
}
};

// Description: Logout
// Endpoint: POST /api/auth/logout
// Endpoint: POST /auth/logout
// Request: {}
// Response: { success: boolean, message: string }
export const logout = async () => {
try {
return await api.post("/api/auth/logout");
return await api.post("/auth/logout");
} catch (error) {
throw new Error(error?.response?.data?.message || error.message);
}
};
};
Loading