Use UTF-8 safe return-to encoding and decoding across router and login
Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
import { createRouter, createWebHistory } from "vue-router";
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
import { useLoginStore } from "@/stores/login";
|
import { useLoginStore } from "@/stores/login";
|
||||||
import { getCookie, setCookie } from "@/utils/cookieUtil";
|
import { getCookie, setCookie } from "@/utils/cookieUtil";
|
||||||
|
import { encodeReturnTo } from "@/utils/returnToEncoding";
|
||||||
import AuthContainer from "@/views/AuthContainer.vue";
|
import AuthContainer from "@/views/AuthContainer.vue";
|
||||||
import MainContainer from "@/views/MainContainer.vue";
|
import MainContainer from "@/views/MainContainer.vue";
|
||||||
import Login from "@/views/Login/LoginPage.vue";
|
import Login from "@/views/Login/LoginPage.vue";
|
||||||
@@ -182,7 +183,7 @@ function buildLoginRedirect(fullPath: string) {
|
|||||||
return {
|
return {
|
||||||
path: "/login",
|
path: "/login",
|
||||||
query: {
|
query: {
|
||||||
"return-to": btoa(fullPath),
|
"return-to": encodeReturnTo(fullPath),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { defineStore } from "pinia";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import apiClient from "@/api/client.js";
|
import apiClient from "@/api/client.js";
|
||||||
import apiError from "@/module/apiError.js";
|
import apiError from "@/module/apiError.js";
|
||||||
|
import { decodeReturnTo } from "@/utils/returnToEncoding";
|
||||||
import {
|
import {
|
||||||
deleteCookie,
|
deleteCookie,
|
||||||
setCookie,
|
setCookie,
|
||||||
@@ -73,7 +74,7 @@ export const useLoginStore = defineStore("loginStore", {
|
|||||||
if (this.rememberedReturnToUrl !== "") {
|
if (this.rememberedReturnToUrl !== "") {
|
||||||
let decodedUrl = "";
|
let decodedUrl = "";
|
||||||
try {
|
try {
|
||||||
decodedUrl = atob(this.rememberedReturnToUrl);
|
decodedUrl = decodeReturnTo(this.rememberedReturnToUrl);
|
||||||
} catch {
|
} catch {
|
||||||
this.$router.push("/files");
|
this.$router.push("/files");
|
||||||
return;
|
return;
|
||||||
|
|||||||
30
src/utils/returnToEncoding.js
Normal file
30
src/utils/returnToEncoding.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// The Lucia project.
|
||||||
|
// Copyright 2026-2026 DSP, inc. All rights reserved.
|
||||||
|
// Authors:
|
||||||
|
// imacat.yang@dsp.im (imacat), 2026/03/08
|
||||||
|
/** @module returnToEncoding UTF-8 safe base64 helpers for return-to paths. */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a return-to path into UTF-8 safe base64.
|
||||||
|
* @param {string} returnToPath - The relative path to preserve.
|
||||||
|
* @returns {string} Base64-encoded UTF-8 string.
|
||||||
|
*/
|
||||||
|
export function encodeReturnTo(returnToPath) {
|
||||||
|
const bytes = new TextEncoder().encode(returnToPath);
|
||||||
|
let binary = "";
|
||||||
|
bytes.forEach((byte) => {
|
||||||
|
binary += String.fromCharCode(byte);
|
||||||
|
});
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes a UTF-8 safe base64 return-to payload back to a path.
|
||||||
|
* @param {string} encodedReturnTo - Base64 payload from query parameter.
|
||||||
|
* @returns {string} Decoded path string.
|
||||||
|
*/
|
||||||
|
export function decodeReturnTo(encodedReturnTo) {
|
||||||
|
const binary = atob(encodedReturnTo);
|
||||||
|
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
||||||
|
return new TextDecoder().decode(bytes);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
// imacat.yang@dsp.im (imacat), 2026/03/06
|
// imacat.yang@dsp.im (imacat), 2026/03/06
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach } from "vitest";
|
import { describe, it, expect, beforeEach } from "vitest";
|
||||||
|
import { decodeReturnTo } from "@/utils/returnToEncoding";
|
||||||
|
|
||||||
describe("router beforeEach guard logic", () => {
|
describe("router beforeEach guard logic", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -70,7 +71,7 @@ describe("router beforeEach guard logic", () => {
|
|||||||
matched: [{ meta: { requiresAuth: true } }],
|
matched: [{ meta: { requiresAuth: true } }],
|
||||||
});
|
});
|
||||||
expect(result.path).toBe("/login");
|
expect(result.path).toBe("/login");
|
||||||
expect(atob(result.query["return-to"])).toBe("/files");
|
expect(decodeReturnTo(result.query["return-to"])).toBe("/files");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows requiresAuth route when refresh token can refresh session", () => {
|
it("allows requiresAuth route when refresh token can refresh session", () => {
|
||||||
@@ -100,7 +101,7 @@ describe("router beforeEach guard logic", () => {
|
|||||||
{ refreshSucceeds: false },
|
{ refreshSucceeds: false },
|
||||||
);
|
);
|
||||||
expect(result.path).toBe("/login");
|
expect(result.path).toBe("/login");
|
||||||
expect(atob(result.query["return-to"])).toBe(
|
expect(decodeReturnTo(result.query["return-to"])).toBe(
|
||||||
"/discover/log/1/map?view=summary#node-2",
|
"/discover/log/1/map?view=summary#node-2",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
21
tests/unit/utils/returnToEncoding.test.js
Normal file
21
tests/unit/utils/returnToEncoding.test.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// The Lucia project.
|
||||||
|
// Copyright 2026-2026 DSP, inc. All rights reserved.
|
||||||
|
// Authors:
|
||||||
|
// imacat.yang@dsp.im (imacat), 2026/03/08
|
||||||
|
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { encodeReturnTo, decodeReturnTo } from "@/utils/returnToEncoding";
|
||||||
|
|
||||||
|
describe("returnToEncoding", () => {
|
||||||
|
it("round-trips ASCII paths", () => {
|
||||||
|
const original = "/discover/log/1/map?view=summary#node-2";
|
||||||
|
const encoded = encodeReturnTo(original);
|
||||||
|
expect(decodeReturnTo(encoded)).toBe(original);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("round-trips Unicode paths without throwing", () => {
|
||||||
|
const original = "/files?keyword=流程圖#節點";
|
||||||
|
const encoded = encodeReturnTo(original);
|
||||||
|
expect(decodeReturnTo(encoded)).toBe(original);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user