Compare commits

...

4 Commits

Author SHA1 Message Date
Sosthene
59bfa9bd55 jwt debug
All checks were successful
Test - Build & Deploy to Scaleway / build-and-push-image-lecoffre (push) Successful in 1m18s
Test - Build & Deploy to Scaleway / deploy-to-scaleway-lecoffre (push) Successful in 3s
2025-08-03 18:35:09 +02:00
Sosthene
66f4c65158 Make authentication more reliable
All checks were successful
Test - Build & Deploy to Scaleway / build-and-push-image-lecoffre (push) Successful in 1m17s
Test - Build & Deploy to Scaleway / deploy-to-scaleway-lecoffre (push) Successful in 3s
2025-08-03 18:05:23 +02:00
Sosthene
6217cfb170 update test workflow
All checks were successful
Test - Build & Deploy to Scaleway / build-and-push-image-lecoffre (push) Successful in 1m37s
Test - Build & Deploy to Scaleway / deploy-to-scaleway-lecoffre (push) Successful in 3s
2025-08-03 17:24:43 +02:00
Sosthene
ea7e74ba06 Add verbosity
All checks were successful
Test - Build & Deploy to Scaleway / build-and-push-image-lecoffre (push) Successful in 12s
Test - Build & Deploy to Scaleway / deploy-to-scaleway-lecoffre (push) Successful in 3s
2025-08-03 17:10:10 +02:00
11 changed files with 381 additions and 32 deletions

View File

@ -17,8 +17,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: cicd
- name: Setup SSH
run: |
mkdir -p ~/.ssh

View File

@ -30,6 +30,15 @@ export default abstract class BaseApiService {
}
protected buildHeaders(contentType: ContentType) {
// Don't try to access cookies during server-side rendering
if (typeof window === 'undefined') {
const headers = new Headers();
if (contentType === ContentType.JSON || contentType === ContentType.PDF) {
headers.set("Content-Type", contentType);
}
return headers;
}
const token = CookieService.getInstance().getCookie("leCoffreAccessToken");
const headers = new Headers();
@ -37,7 +46,14 @@ export default abstract class BaseApiService {
if (contentType === ContentType.JSON || contentType === ContentType.PDF) {
headers.set("Content-Type", contentType);
}
headers.set("Authorization", `Bearer ${token}`);
// Only set Authorization header if token exists
if (token) {
headers.set("Authorization", `Bearer ${token}`);
} else {
console.warn("No access token found in cookies when building headers");
}
return headers;
}
@ -134,38 +150,66 @@ export default abstract class BaseApiService {
}
private async checkJwtToken() {
// Don't check tokens during server-side rendering
if (typeof window === 'undefined') {
return;
}
const accessToken = CookieService.getInstance().getCookie("leCoffreAccessToken");
if (!accessToken) return;
const userDecodedToken = jwt_decode(accessToken) as IUserJwtPayload;
const customerDecodedToken = jwt_decode(accessToken) as ICustomerJwtPayload;
if (!userDecodedToken && !customerDecodedToken) return;
const now = Math.floor(Date.now() / 1000);
if (userDecodedToken.userId && userDecodedToken.exp < now) {
const refreshToken = CookieService.getInstance().getCookie("leCoffreRefreshToken");
if (!refreshToken) {
return;
}
const decodedRefreshToken = jwt_decode(refreshToken) as IUserJwtPayload | ICustomerJwtPayload;
if (decodedRefreshToken.exp < now) {
return;
}
await JwtService.getInstance().refreshToken(refreshToken);
if (!accessToken) {
console.warn("No access token found during JWT check");
return;
}
if (customerDecodedToken.customerId && customerDecodedToken.exp < now) {
const refreshToken = CookieService.getInstance().getCookie("leCoffreRefreshToken");
if (!refreshToken) {
try {
const userDecodedToken = jwt_decode(accessToken) as IUserJwtPayload;
const customerDecodedToken = jwt_decode(accessToken) as ICustomerJwtPayload;
if (!userDecodedToken && !customerDecodedToken) {
console.warn("Invalid token format during JWT check");
return;
}
const decodedRefreshToken = jwt_decode(refreshToken) as IUserJwtPayload | ICustomerJwtPayload;
if (decodedRefreshToken.exp < now) {
return;
const now = Math.floor(Date.now() / 1000);
if (userDecodedToken.userId && userDecodedToken.exp < now) {
const refreshToken = CookieService.getInstance().getCookie("leCoffreRefreshToken");
if (!refreshToken) {
console.warn("Access token expired but no refresh token found");
return;
}
const decodedRefreshToken = jwt_decode(refreshToken) as IUserJwtPayload | ICustomerJwtPayload;
if (decodedRefreshToken.exp < now) {
console.warn("Both access and refresh tokens are expired");
return;
}
const refreshSuccess = await JwtService.getInstance().refreshToken(refreshToken);
if (!refreshSuccess) {
console.error("Failed to refresh token");
return;
}
}
await JwtService.getInstance().refreshToken(refreshToken);
if (customerDecodedToken.customerId && customerDecodedToken.exp < now) {
const refreshToken = CookieService.getInstance().getCookie("leCoffreRefreshToken");
if (!refreshToken) {
console.warn("Access token expired but no refresh token found");
return;
}
const decodedRefreshToken = jwt_decode(refreshToken) as IUserJwtPayload | ICustomerJwtPayload;
if (decodedRefreshToken.exp < now) {
console.warn("Both access and refresh tokens are expired");
return;
}
const refreshSuccess = await JwtService.getInstance().refreshToken(refreshToken);
if (!refreshSuccess) {
console.error("Failed to refresh token");
return;
}
}
} catch (error) {
console.error("Error during JWT token check:", error);
}
return;
}
protected async processResponse<T>(response: Response, request: () => Promise<Response>, ref?: IRef, fileName?: string): Promise<T> {

View File

@ -0,0 +1,122 @@
import React, { useEffect, useState } from "react";
import JwtService from "@Front/Services/JwtService/JwtService";
interface JwtDebuggerProps {
expectedRules?: string[];
showAlways?: boolean;
}
export default function JwtDebugger({ expectedRules = [], showAlways = false }: JwtDebuggerProps) {
const [isVisible, setIsVisible] = useState(showAlways);
const [debugInfo, setDebugInfo] = useState<any>(null);
useEffect(() => {
// Only show in development
if (process.env.NODE_ENV !== "development" && !showAlways) {
return;
}
const jwt = JwtService.getInstance().debugJwtToken();
if (jwt) {
setDebugInfo(jwt);
}
}, [showAlways]);
// Keyboard shortcut to toggle debugger (Ctrl+Shift+D)
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.ctrlKey && e.shiftKey && e.key === "D") {
e.preventDefault();
setIsVisible(!isVisible);
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isVisible]);
if (!isVisible) return null;
return (
<div style={{
position: "fixed",
top: "10px",
right: "10px",
background: "#1a1a1a",
color: "#fff",
padding: "15px",
borderRadius: "8px",
fontSize: "12px",
fontFamily: "monospace",
maxWidth: "400px",
maxHeight: "80vh",
overflow: "auto",
zIndex: 9999,
border: "1px solid #333"
}}>
<div style={{ marginBottom: "10px", display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<strong>JWT Debugger</strong>
<button
onClick={() => setIsVisible(false)}
style={{ background: "none", border: "none", color: "#fff", cursor: "pointer" }}
>
</button>
</div>
{debugInfo && (
<div>
<div><strong>User ID:</strong> {debugInfo.userId}</div>
<div><strong>Email:</strong> {debugInfo.email}</div>
<div><strong>Role:</strong> {debugInfo.role}</div>
<div><strong>Office ID:</strong> {debugInfo.office_Id}</div>
<div><strong>Rules Count:</strong> {debugInfo.rules?.length || 0}</div>
<div><strong>Rules:</strong></div>
<ul style={{ margin: "5px 0", paddingLeft: "20px" }}>
{debugInfo.rules?.map((rule: string, index: number) => (
<li key={index}>{rule}</li>
))}
</ul>
<div><strong>Expires:</strong> {new Date(debugInfo.exp * 1000).toLocaleString()}</div>
{expectedRules.length > 0 && (
<div style={{ marginTop: "15px" }}>
<div><strong>Expected Rules:</strong></div>
<ul style={{ margin: "5px 0", paddingLeft: "20px" }}>
{expectedRules.map((rule, index) => (
<li key={index} style={{
color: debugInfo.rules?.includes(rule) ? "#4ade80" : "#f87171"
}}>
{rule} {debugInfo.rules?.includes(rule) ? "✓" : "✗"}
</li>
))}
</ul>
</div>
)}
<div style={{ marginTop: "15px" }}>
<strong>Quick Tests:</strong>
<div style={{ marginTop: "5px" }}>
<button
onClick={() => JwtService.getInstance().checkSpecificRule("folders", "GET")}
style={{ marginRight: "5px", padding: "2px 5px", fontSize: "10px" }}
>
Test GET folders
</button>
<button
onClick={() => JwtService.getInstance().checkSpecificRule("users", "POST")}
style={{ marginRight: "5px", padding: "2px 5px", fontSize: "10px" }}
>
Test POST users
</button>
</div>
</div>
</div>
)}
<div style={{ marginTop: "10px", fontSize: "10px", color: "#888" }}>
Press Ctrl+Shift+D to toggle
</div>
</div>
);
}

View File

@ -36,6 +36,7 @@ export default function Header(props: IProps) {
const loadSubscription = useCallback(async () => {
const jwt = JwtService.getInstance().decodeJwt();
console.log("jwt:", jwt);
const subscription = await Subscriptions.getInstance().get({ where: { office: { uid: jwt?.office_Id } } });
if (subscription[0]) {
const stripeSubscription = await Stripe.getInstance().getStripeSubscriptionByUid(subscription[0].stripe_subscription_id!);

View File

@ -18,6 +18,7 @@ export default function SubscriptionError() {
const loadSubscription = useCallback(async () => {
const jwt = JwtService.getInstance().decodeJwt();
console.log("jwt:", jwt);
const subscription = await Subscriptions.getInstance().get({ where: { office: { uid: jwt?.office_Id } } });
if (!subscription[0]) return;
setSubscription(subscription[0]);

View File

@ -42,6 +42,7 @@ export default function SubscriptionFacturation() {
const manageSubscription = async () => {
try {
const jwt = JwtService.getInstance().decodeJwt();
console.log("jwt:", jwt);
const subscription = await Subscriptions.getInstance().get({ where: { office: { uid: jwt?.office_Id } } });
if (!subscription[0]) return;
const stripe_client_portal = await Stripe.getInstance().getClientPortalSession(subscription[0].stripe_subscription_id!);
@ -52,6 +53,7 @@ export default function SubscriptionFacturation() {
const cancelOrReactivateSubscription = async () => {
try {
const jwt = JwtService.getInstance().decodeJwt();
console.log("jwt:", jwt);
const subscription = await Subscriptions.getInstance().get({ where: { office: { uid: jwt?.office_Id } } });
if (!subscription[0]) return;
const stripe_client_portal = await Stripe.getInstance().getClientPortalSession(subscription[0].stripe_subscription_id!);
@ -66,6 +68,7 @@ export default function SubscriptionFacturation() {
const manageBilling = async () => {
try {
const jwt = JwtService.getInstance().decodeJwt();
console.log("jwt:", jwt);
const subscription = await Subscriptions.getInstance().get({ where: { office: { uid: jwt?.office_Id } } });
if (!subscription[0]) return;
const stripe_client_portal = await Stripe.getInstance().getClientPortalSession(subscription[0].stripe_subscription_id!);
@ -75,6 +78,7 @@ export default function SubscriptionFacturation() {
const loadSubscription = useCallback(async () => {
const jwt = JwtService.getInstance().decodeJwt();
console.log("jwt:", jwt);
const subscription = await Subscriptions.getInstance().get({ where: { office: { uid: jwt?.office_Id } } });
if (!subscription[0]) {
router.push(Module.getInstance().get().modules.pages.Subscription.pages.New.props.path);

View File

@ -21,6 +21,7 @@ export default function SubscriptionSuccess() {
const loadSubscription = useCallback(async () => {
const jwt = JwtService.getInstance().decodeJwt();
console.log("jwt:", jwt);
const subscription = await Subscriptions.getInstance().get({ where: { office: { uid: jwt?.office_Id } } });
if (!subscription[0]) return;
setSubscription(subscription[0]);

View File

@ -1,5 +1,6 @@
import Users from "@Front/Api/LeCoffreApi/Notary/Users/Users";
import JwtService from "@Front/Services/JwtService/JwtService";
import UserStore from "@Front/Stores/UserStore";
import User from "le-coffre-resources/dist/Notary";
import { useEffect, useState } from "react";
@ -7,8 +8,23 @@ export default function useUser() {
const [user, setUser] = useState<User | null>();
useEffect(() => {
// Don't run on server-side
if (typeof window === 'undefined') {
return;
}
// Check if user is connected before making API calls
if (!UserStore.instance.isConnected()) {
console.warn("User not connected, skipping API call");
return;
}
const decodedJwt = JwtService.getInstance().decodeJwt();
if (!decodedJwt) return;
if (!decodedJwt) {
console.warn("No valid JWT found, skipping API call");
return;
}
Users.getInstance()
.getByUid(decodedJwt.userId, {
q: {
@ -17,6 +33,9 @@ export default function useUser() {
})
.then((user) => {
setUser(user);
})
.catch((error) => {
console.error("Failed to fetch user:", error);
});
}, []);

View File

@ -165,4 +165,75 @@ export default class JwtService {
if (!token) return false;
return token?.rules?.some((rule: string) => rule === `${action} ${name}`);
}
/**
* Debug method to log JWT token details including rules
*/
public debugJwtToken() {
const token = this.decodeJwt();
if (!token) {
console.warn("No JWT token found");
return null;
}
console.log("=== JWT Token Debug Info ===");
console.log("User ID:", token.userId);
console.log("Email:", token.email);
console.log("Role:", token.role);
console.log("Office ID:", token.office_Id);
console.log("Rules count:", token.rules?.length || 0);
console.log("Rules:", token.rules);
console.log("Expiration:", new Date(token.exp * 1000).toISOString());
console.log("=============================");
return token;
}
/**
* Check if a specific rule exists in the JWT token
*/
public checkSpecificRule(name: string, action: string) {
const token = this.decodeJwt();
if (!token) {
console.warn("No JWT token found");
return false;
}
const expectedRule = `${action} ${name}`;
const hasRule = token?.rules?.some((rule: string) => rule === expectedRule);
console.log(`=== Rule Check: ${expectedRule} ===`);
console.log("Expected rule:", expectedRule);
console.log("Available rules:", token.rules);
console.log("Rule found:", hasRule);
console.log("=============================");
return hasRule;
}
/**
* Compare JWT rules with expected rules from database
*/
public compareRulesWithDatabase(expectedRules: string[]) {
const token = this.decodeJwt();
if (!token) {
console.warn("No JWT token found");
return { missing: expectedRules, extra: [], matches: [] };
}
const jwtRules = token.rules || [];
const missing = expectedRules.filter(rule => !jwtRules.includes(rule));
const extra = jwtRules.filter(rule => !expectedRules.includes(rule));
const matches = jwtRules.filter(rule => expectedRules.includes(rule));
console.log("=== Rules Comparison ===");
console.log("Expected rules (from DB):", expectedRules);
console.log("JWT rules:", jwtRules);
console.log("Missing rules:", missing);
console.log("Extra rules:", extra);
console.log("Matching rules:", matches);
console.log("=========================");
return { missing, extra, matches };
}
}

View File

@ -9,10 +9,46 @@ export default class UserStore {
protected readonly event = new EventEmitter();
public accessToken: string | null = null;
public refreshToken: string | null = null;
private initialized = false;
private constructor() {}
private constructor() {
// Don't initialize tokens during server-side rendering
if (typeof window !== 'undefined') {
this.initializeFromCookies();
}
}
private initializeFromCookies() {
if (this.initialized) return;
try {
const accessToken = CookieService.getInstance().getCookie("leCoffreAccessToken");
const refreshToken = CookieService.getInstance().getCookie("leCoffreRefreshToken");
if (accessToken) {
this.accessToken = accessToken;
}
if (refreshToken) {
this.refreshToken = refreshToken;
}
this.initialized = true;
} catch (error) {
console.warn("Failed to initialize tokens from cookies:", error);
}
}
public isConnected(): boolean {
// Ensure initialization on client side
if (typeof window !== 'undefined' && !this.initialized) {
this.initializeFromCookies();
}
// Check both instance variable and cookie to ensure consistency
if (typeof window !== 'undefined') {
const cookieToken = CookieService.getInstance().getCookie("leCoffreAccessToken");
return !!(this.accessToken || cookieToken);
}
return !!this.accessToken;
}
@ -27,6 +63,10 @@ export default class UserStore {
CookieService.getInstance().setCookie("leCoffreAccessToken", accessToken);
CookieService.getInstance().setCookie("leCoffreRefreshToken", refreshToken);
// Update instance variables
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.event.emit("connection", this.accessToken);
} catch (error) {
console.error(error);
@ -41,6 +81,10 @@ export default class UserStore {
CookieService.getInstance().deleteCookie("leCoffreAccessToken");
CookieService.getInstance().deleteCookie("leCoffreRefreshToken");
// Clear instance variables
this.accessToken = null;
this.refreshToken = null;
this.event.emit("disconnection", this.accessToken);
} catch (error) {
console.error(error);

View File

@ -9,10 +9,46 @@ export default class UserStore {
protected readonly event = new EventEmitter();
public accessToken: string | null = null;
public refreshToken: string | null = null;
private initialized = false;
private constructor() {}
private constructor() {
// Don't initialize tokens during server-side rendering
if (typeof window !== 'undefined') {
this.initializeFromCookies();
}
}
private initializeFromCookies() {
if (this.initialized) return;
try {
const accessToken = CookieService.getInstance().getCookie("leCoffreAccessToken");
const refreshToken = CookieService.getInstance().getCookie("leCoffreRefreshToken");
if (accessToken) {
this.accessToken = accessToken;
}
if (refreshToken) {
this.refreshToken = refreshToken;
}
this.initialized = true;
} catch (error) {
console.warn("Failed to initialize tokens from cookies:", error);
}
}
public isConnected(): boolean {
// Ensure initialization on client side
if (typeof window !== 'undefined' && !this.initialized) {
this.initializeFromCookies();
}
// Check both instance variable and cookie to ensure consistency
if (typeof window !== 'undefined') {
const cookieToken = CookieService.getInstance().getCookie("leCoffreAccessToken");
return !!(this.accessToken || cookieToken);
}
return !!this.accessToken;
}
@ -27,6 +63,10 @@ export default class UserStore {
CookieService.getInstance().setCookie("leCoffreAccessToken", accessToken);
CookieService.getInstance().setCookie("leCoffreRefreshToken", refreshToken);
// Update instance variables
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.event.emit("connection", this.accessToken);
} catch (error) {
console.error(error);
@ -41,6 +81,10 @@ export default class UserStore {
CookieService.getInstance().deleteCookie("leCoffreAccessToken");
CookieService.getInstance().deleteCookie("leCoffreRefreshToken");
// Clear instance variables
this.accessToken = null;
this.refreshToken = null;
this.event.emit("disconnection", this.accessToken);
} catch (error) {
console.error(error);