diff --git a/internal/http/response/shortcuts.go b/internal/http/response/shortcuts.go index dec01c9fa..d94936147 100644 --- a/internal/http/response/shortcuts.go +++ b/internal/http/response/shortcuts.go @@ -2,6 +2,7 @@ package response import ( "net/http" + "net/url" "github.com/gin-gonic/gin" ) @@ -35,10 +36,17 @@ func SendInternalServerError(ctx *gin.Context) { } // SendNotFound directly sends a not found response -func RedirectToLogin(ctx *gin.Context, dst string) { - ctx.Redirect(http.StatusFound, "/login?dst="+dst) +func RedirectToLogin(ctx *gin.Context, webroot, dst string) { + url := url.URL{ + Path: webroot, + RawQuery: url.Values{ + "dst": []string{dst}, + }.Encode(), + } + ctx.Redirect(http.StatusFound, url.String()) } +// NotFound directly sends a not found response func NotFound(ctx *gin.Context) { ctx.AbortWithStatus(http.StatusNotFound) } diff --git a/internal/http/routes/bookmark.go b/internal/http/routes/bookmark.go index e0073dcdd..7a2c7e8a8 100644 --- a/internal/http/routes/bookmark.go +++ b/internal/http/routes/bookmark.go @@ -64,7 +64,7 @@ func (r *BookmarkRoutes) getBookmark(c *context.Context) (*model.BookmarkDTO, er } if bookmark.Public != 1 && !c.UserIsLogged() { - response.RedirectToLogin(c.Context, c.Request.URL.String()) + response.RedirectToLogin(c.Context, r.deps.Config.Http.RootPath, c.Request.URL.String()) return nil, model.ErrUnauthorized } diff --git a/internal/http/routes/bookmark_test.go b/internal/http/routes/bookmark_test.go index 400dd95b2..9ed14b506 100644 --- a/internal/http/routes/bookmark_test.go +++ b/internal/http/routes/bookmark_test.go @@ -4,6 +4,7 @@ import ( "context" "net/http" "net/http/httptest" + "net/url" "strconv" "testing" @@ -122,7 +123,7 @@ func TestBookmarkContentHandler(t *testing.T) { req, _ := http.NewRequest("GET", path, nil) g.ServeHTTP(w, req) require.Equal(t, http.StatusFound, w.Code) - require.Equal(t, "/login?dst="+path, w.Header().Get("Location")) + require.Equal(t, "/?dst="+url.QueryEscape(path), w.Header().Get("Location")) }) t.Run("get existing bookmark content", func(t *testing.T) { diff --git a/internal/view/assets/js/component/login.js b/internal/view/assets/js/component/login.js index 4e74ba25b..e75925556 100644 --- a/internal/view/assets/js/component/login.js +++ b/internal/view/assets/js/component/login.js @@ -39,10 +39,35 @@ export default { username: "", password: "", remember: false, + destination: "/", // Default destination }; }, emits: ["login-success"], methods: { + sanitizeDestination(dst) { + try { + // Remove any leading/trailing whitespace + dst = dst.trim(); + + // Decode the URL to handle any encoded characters + dst = decodeURIComponent(dst); + + // Create a URL object to parse the destination + const url = new URL(dst, window.location.origin); + + // Only allow paths from the same origin + if (url.origin !== window.location.origin) { + return "/"; + } + + // Only return the pathname and search params + return url.pathname + url.search + url.hash; + } catch (e) { + // If any error occurs during parsing, return root + return "/"; + } + }, + async getErrorMessage(err) { switch (err.constructor) { case Error: @@ -119,6 +144,9 @@ export default { this.visible = false; this.$emit("login-success"); + + // Redirect to sanitized destination + if (this.destination !== "/") window.location.href = this.destination; }) .catch((err) => { this.loading = false; @@ -129,6 +157,11 @@ export default { }, }, async mounted() { + // Get and sanitize destination from URL parameters + const urlParams = new URLSearchParams(window.location.search); + const dst = urlParams.get("dst"); + this.destination = dst ? this.sanitizeDestination(dst) : "/"; + // Check if there's a valid session first const token = localStorage.getItem("shiori-token"); if (token) { diff --git a/internal/view/index.html b/internal/view/index.html index 177cc542f..c23f677a0 100644 --- a/internal/view/index.html +++ b/internal/view/index.html @@ -105,6 +105,8 @@ document.cookie = `token=; Path=${new URL(document.baseURI).pathname}; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`; this.isLoggedIn = false; this.loginRequired = true; + this.dialog.loading = false; + this.dialog.visible = false; }).catch(err => { this.dialog.loading = false; this.getErrorMessage(err).then(msg => { @@ -155,7 +157,7 @@ owner: owner, }; }, - + onLoginSuccess() { this.loadSetting(); this.loadAccount(); @@ -165,7 +167,7 @@ async validateSession() { const token = localStorage.getItem("shiori-token"); const account = localStorage.getItem("shiori-account"); - + if (!(token && account)) { return false; } @@ -176,11 +178,11 @@ "Authorization": `Bearer ${token}` } }); - + if (!response.ok) { throw new Error('Invalid session'); } - + return true; } catch (err) { // Clear invalid session data @@ -195,7 +197,7 @@ async checkLoginStatus() { const isValid = await this.validateSession(); this.isLoggedIn = isValid; - + if (isValid) { this.loadSetting(); this.loadAccount();