Spaces:
Running
Running
fix login
Browse files- app/api/auth/logout/route.ts +25 -0
- app/api/auth/route.ts +21 -1
- hooks/useUser.ts +41 -11
- lib/api.ts +1 -1
- lib/iframe-storage.ts +76 -0
app/api/auth/logout/route.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from "next/server";
|
| 2 |
+
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
| 3 |
+
|
| 4 |
+
export async function POST() {
|
| 5 |
+
const cookieName = MY_TOKEN_KEY();
|
| 6 |
+
const isProduction = process.env.NODE_ENV === "production";
|
| 7 |
+
|
| 8 |
+
const response = NextResponse.json(
|
| 9 |
+
{ message: "Logged out successfully" },
|
| 10 |
+
{ status: 200 }
|
| 11 |
+
);
|
| 12 |
+
|
| 13 |
+
// Clear the HTTP-only cookie
|
| 14 |
+
const cookieOptions = [
|
| 15 |
+
`${cookieName}=`,
|
| 16 |
+
"Max-Age=0",
|
| 17 |
+
"Path=/",
|
| 18 |
+
"HttpOnly",
|
| 19 |
+
...(isProduction ? ["Secure", "SameSite=None"] : ["SameSite=Lax"])
|
| 20 |
+
].join("; ");
|
| 21 |
+
|
| 22 |
+
response.headers.set("Set-Cookie", cookieOptions);
|
| 23 |
+
|
| 24 |
+
return response;
|
| 25 |
+
}
|
app/api/auth/route.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
|
|
|
| 2 |
|
| 3 |
export async function POST(req: NextRequest) {
|
| 4 |
const body = await req.json();
|
|
@@ -70,11 +71,17 @@ export async function POST(req: NextRequest) {
|
|
| 70 |
}
|
| 71 |
const user = await userResponse.json();
|
| 72 |
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
{
|
| 75 |
access_token: response.access_token,
|
| 76 |
expires_in: response.expires_in,
|
| 77 |
user,
|
|
|
|
|
|
|
| 78 |
},
|
| 79 |
{
|
| 80 |
status: 200,
|
|
@@ -83,4 +90,17 @@ export async function POST(req: NextRequest) {
|
|
| 83 |
},
|
| 84 |
}
|
| 85 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
}
|
|
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
| 3 |
|
| 4 |
export async function POST(req: NextRequest) {
|
| 5 |
const body = await req.json();
|
|
|
|
| 71 |
}
|
| 72 |
const user = await userResponse.json();
|
| 73 |
|
| 74 |
+
const cookieName = MY_TOKEN_KEY();
|
| 75 |
+
const isProduction = process.env.NODE_ENV === "production";
|
| 76 |
+
|
| 77 |
+
// Create response with user data
|
| 78 |
+
const nextResponse = NextResponse.json(
|
| 79 |
{
|
| 80 |
access_token: response.access_token,
|
| 81 |
expires_in: response.expires_in,
|
| 82 |
user,
|
| 83 |
+
// Include fallback flag for iframe contexts
|
| 84 |
+
useLocalStorageFallback: true,
|
| 85 |
},
|
| 86 |
{
|
| 87 |
status: 200,
|
|
|
|
| 90 |
},
|
| 91 |
}
|
| 92 |
);
|
| 93 |
+
|
| 94 |
+
// Set HTTP-only cookie with proper attributes for iframe support
|
| 95 |
+
const cookieOptions = [
|
| 96 |
+
`${cookieName}=${response.access_token}`,
|
| 97 |
+
`Max-Age=${response.expires_in || 3600}`, // Default 1 hour if not provided
|
| 98 |
+
"Path=/",
|
| 99 |
+
"HttpOnly",
|
| 100 |
+
...(isProduction ? ["Secure", "SameSite=None"] : ["SameSite=Lax"])
|
| 101 |
+
].join("; ");
|
| 102 |
+
|
| 103 |
+
nextResponse.headers.set("Set-Cookie", cookieOptions);
|
| 104 |
+
|
| 105 |
+
return nextResponse;
|
| 106 |
}
|
hooks/useUser.ts
CHANGED
|
@@ -5,26 +5,36 @@ import { useCookie } from "react-use";
|
|
| 5 |
import { useRouter } from "next/navigation";
|
| 6 |
|
| 7 |
import { User } from "@/types";
|
| 8 |
-
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
| 9 |
import { api } from "@/lib/api";
|
| 10 |
import { toast } from "sonner";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
|
| 13 |
export const useUser = (initialData?: {
|
| 14 |
user: User | null;
|
| 15 |
errCode: number | null;
|
| 16 |
}) => {
|
| 17 |
-
const cookie_name = MY_TOKEN_KEY();
|
| 18 |
const client = useQueryClient();
|
| 19 |
const router = useRouter();
|
| 20 |
-
const [, setCookie, removeCookie] = useCookie(cookie_name);
|
| 21 |
const [currentRoute, setCurrentRoute, removeCurrentRoute] = useCookie("deepsite-currentRoute");
|
| 22 |
|
| 23 |
const { data: { user, errCode } = { user: null, errCode: null }, isLoading } =
|
| 24 |
useQuery({
|
| 25 |
queryKey: ["user.me"],
|
| 26 |
queryFn: async () => {
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
},
|
| 29 |
refetchOnWindowFocus: false,
|
| 30 |
refetchOnReconnect: false,
|
|
@@ -59,7 +69,12 @@ export const useUser = (initialData?: {
|
|
| 59 |
.post("/auth", { code })
|
| 60 |
.then(async (res: any) => {
|
| 61 |
if (res.data) {
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
client.setQueryData(["user.me"], {
|
| 64 |
user: res.data.user,
|
| 65 |
errCode: null,
|
|
@@ -82,12 +97,27 @@ export const useUser = (initialData?: {
|
|
| 82 |
};
|
| 83 |
|
| 84 |
const logout = async () => {
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
};
|
| 92 |
|
| 93 |
return {
|
|
|
|
| 5 |
import { useRouter } from "next/navigation";
|
| 6 |
|
| 7 |
import { User } from "@/types";
|
|
|
|
| 8 |
import { api } from "@/lib/api";
|
| 9 |
import { toast } from "sonner";
|
| 10 |
+
import {
|
| 11 |
+
storeAuthDataFallback,
|
| 12 |
+
getAuthDataFallback,
|
| 13 |
+
clearAuthDataFallback,
|
| 14 |
+
isInIframe
|
| 15 |
+
} from "@/lib/iframe-storage";
|
| 16 |
|
| 17 |
|
| 18 |
export const useUser = (initialData?: {
|
| 19 |
user: User | null;
|
| 20 |
errCode: number | null;
|
| 21 |
}) => {
|
|
|
|
| 22 |
const client = useQueryClient();
|
| 23 |
const router = useRouter();
|
|
|
|
| 24 |
const [currentRoute, setCurrentRoute, removeCurrentRoute] = useCookie("deepsite-currentRoute");
|
| 25 |
|
| 26 |
const { data: { user, errCode } = { user: null, errCode: null }, isLoading } =
|
| 27 |
useQuery({
|
| 28 |
queryKey: ["user.me"],
|
| 29 |
queryFn: async () => {
|
| 30 |
+
// Check for fallback data if no initial data provided and we're in iframe
|
| 31 |
+
if (!initialData && isInIframe()) {
|
| 32 |
+
const fallbackData = getAuthDataFallback();
|
| 33 |
+
if (fallbackData.user && fallbackData.token) {
|
| 34 |
+
return { user: fallbackData.user, errCode: null };
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
return { user: initialData?.user || null, errCode: initialData?.errCode || null };
|
| 38 |
},
|
| 39 |
refetchOnWindowFocus: false,
|
| 40 |
refetchOnReconnect: false,
|
|
|
|
| 69 |
.post("/auth", { code })
|
| 70 |
.then(async (res: any) => {
|
| 71 |
if (res.data) {
|
| 72 |
+
// Cookie is now set server-side with proper iframe attributes
|
| 73 |
+
// Also store fallback data for iframe contexts
|
| 74 |
+
if (res.data.useLocalStorageFallback) {
|
| 75 |
+
storeAuthDataFallback(res.data.access_token, res.data.user);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
client.setQueryData(["user.me"], {
|
| 79 |
user: res.data.user,
|
| 80 |
errCode: null,
|
|
|
|
| 97 |
};
|
| 98 |
|
| 99 |
const logout = async () => {
|
| 100 |
+
try {
|
| 101 |
+
// Call server endpoint to clear the HTTP-only cookie
|
| 102 |
+
await api.post("/auth/logout");
|
| 103 |
+
// Clear fallback storage
|
| 104 |
+
clearAuthDataFallback();
|
| 105 |
+
removeCurrentRoute();
|
| 106 |
+
client.setQueryData(["user.me"], { user: null, errCode: null });
|
| 107 |
+
router.push("/");
|
| 108 |
+
toast.success("Logout successful");
|
| 109 |
+
client.invalidateQueries({ queryKey: ["user.me"] });
|
| 110 |
+
window.location.reload();
|
| 111 |
+
} catch (error) {
|
| 112 |
+
console.error("Logout error:", error);
|
| 113 |
+
// Even if server call fails, clear client state
|
| 114 |
+
clearAuthDataFallback();
|
| 115 |
+
removeCurrentRoute();
|
| 116 |
+
client.setQueryData(["user.me"], { user: null, errCode: null });
|
| 117 |
+
router.push("/");
|
| 118 |
+
toast.success("Logout successful");
|
| 119 |
+
window.location.reload();
|
| 120 |
+
}
|
| 121 |
};
|
| 122 |
|
| 123 |
return {
|
lib/api.ts
CHANGED
|
@@ -14,7 +14,6 @@ export const apiServer = axios.create({
|
|
| 14 |
cache: "no-store",
|
| 15 |
},
|
| 16 |
});
|
| 17 |
-
|
| 18 |
api.interceptors.request.use(
|
| 19 |
async (config) => {
|
| 20 |
// get the token from cookies
|
|
@@ -33,3 +32,4 @@ api.interceptors.request.use(
|
|
| 33 |
return Promise.reject(error);
|
| 34 |
}
|
| 35 |
);
|
|
|
|
|
|
| 14 |
cache: "no-store",
|
| 15 |
},
|
| 16 |
});
|
|
|
|
| 17 |
api.interceptors.request.use(
|
| 18 |
async (config) => {
|
| 19 |
// get the token from cookies
|
|
|
|
| 32 |
return Promise.reject(error);
|
| 33 |
}
|
| 34 |
);
|
| 35 |
+
|
lib/iframe-storage.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Utility for handling storage in iframe contexts
|
| 3 |
+
* Falls back to localStorage when cookies are blocked
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
export const isInIframe = (): boolean => {
|
| 7 |
+
try {
|
| 8 |
+
return window.self !== window.top;
|
| 9 |
+
} catch {
|
| 10 |
+
return true; // If we can't access window.top, we're likely in an iframe
|
| 11 |
+
}
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
export const STORAGE_KEYS = {
|
| 15 |
+
ACCESS_TOKEN: "deepsite-auth-token-fallback",
|
| 16 |
+
USER_DATA: "deepsite-user-data-fallback",
|
| 17 |
+
} as const;
|
| 18 |
+
|
| 19 |
+
export const iframeStorage = {
|
| 20 |
+
setItem: (key: string, value: string): void => {
|
| 21 |
+
try {
|
| 22 |
+
localStorage.setItem(key, value);
|
| 23 |
+
} catch (error) {
|
| 24 |
+
console.warn("Failed to set localStorage item:", error);
|
| 25 |
+
}
|
| 26 |
+
},
|
| 27 |
+
|
| 28 |
+
getItem: (key: string): string | null => {
|
| 29 |
+
try {
|
| 30 |
+
return localStorage.getItem(key);
|
| 31 |
+
} catch (error) {
|
| 32 |
+
console.warn("Failed to get localStorage item:", error);
|
| 33 |
+
return null;
|
| 34 |
+
}
|
| 35 |
+
},
|
| 36 |
+
|
| 37 |
+
removeItem: (key: string): void => {
|
| 38 |
+
try {
|
| 39 |
+
localStorage.removeItem(key);
|
| 40 |
+
} catch (error) {
|
| 41 |
+
console.warn("Failed to remove localStorage item:", error);
|
| 42 |
+
}
|
| 43 |
+
},
|
| 44 |
+
|
| 45 |
+
clear: (): void => {
|
| 46 |
+
try {
|
| 47 |
+
localStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN);
|
| 48 |
+
localStorage.removeItem(STORAGE_KEYS.USER_DATA);
|
| 49 |
+
} catch (error) {
|
| 50 |
+
console.warn("Failed to clear localStorage items:", error);
|
| 51 |
+
}
|
| 52 |
+
},
|
| 53 |
+
};
|
| 54 |
+
|
| 55 |
+
export const storeAuthDataFallback = (accessToken: string, userData: any): void => {
|
| 56 |
+
if (isInIframe()) {
|
| 57 |
+
iframeStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
|
| 58 |
+
iframeStorage.setItem(STORAGE_KEYS.USER_DATA, JSON.stringify(userData));
|
| 59 |
+
}
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
export const getAuthDataFallback = (): { token: string | null; user: any | null } => {
|
| 63 |
+
if (isInIframe()) {
|
| 64 |
+
const token = iframeStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
|
| 65 |
+
const userDataStr = iframeStorage.getItem(STORAGE_KEYS.USER_DATA);
|
| 66 |
+
const user = userDataStr ? JSON.parse(userDataStr) : null;
|
| 67 |
+
return { token, user };
|
| 68 |
+
}
|
| 69 |
+
return { token: null, user: null };
|
| 70 |
+
};
|
| 71 |
+
|
| 72 |
+
export const clearAuthDataFallback = (): void => {
|
| 73 |
+
if (isInIframe()) {
|
| 74 |
+
iframeStorage.clear();
|
| 75 |
+
}
|
| 76 |
+
};
|