Compare commits

...

2 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
e938ae6f65 feat: support pop-up login UI via CasdoorModal.js SDK
Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com>
2026-03-19 17:49:20 +00:00
copilot-swe-agent[bot]
43fe173b33 Initial plan 2026-03-19 17:34:00 +00:00
4 changed files with 291 additions and 3 deletions

View File

@@ -29,6 +29,7 @@ import (
const (
providerHintRedirectScriptName = "ProviderHintRedirect.js"
authCallbackHandlerScriptName = "AuthCallbackHandler.js"
casdoorModalScriptName = "CasdoorModal.js"
)
func getLightweightAuthScriptPath(scriptName string) string {
@@ -95,6 +96,10 @@ func serveAuthCallbackHandlerScript(ctx *context.Context) bool {
return serveLightweightAuthScript(ctx, "/"+authCallbackHandlerScriptName, authCallbackHandlerScriptName)
}
func serveCasdoorModalScript(ctx *context.Context) bool {
return serveLightweightAuthScript(ctx, "/"+casdoorModalScriptName, casdoorModalScriptName)
}
func serveProviderHintRedirectPage(ctx *context.Context) bool {
if ctx.Request.URL.Path != "/login/oauth/authorize" {
return false

View File

@@ -139,6 +139,9 @@ func StaticFilter(ctx *context.Context) {
if serveProviderHintRedirectScript(ctx) {
return
}
if serveCasdoorModalScript(ctx) {
return
}
if strings.HasPrefix(urlPath, "/cas") && (strings.HasSuffix(urlPath, "/serviceValidate") || strings.HasSuffix(urlPath, "/proxy") || strings.HasSuffix(urlPath, "/proxyValidate") || strings.HasSuffix(urlPath, "/validate") || strings.HasSuffix(urlPath, "/p3/serviceValidate") || strings.HasSuffix(urlPath, "/p3/proxyValidate") || strings.HasSuffix(urlPath, "/samlValidate")) {
return
}

274
web/public/CasdoorModal.js Normal file
View File

@@ -0,0 +1,274 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* CasdoorModal - A lightweight SDK for embedding Casdoor login as a modal popup.
*
* Usage:
* <script src="https://your-casdoor-server/CasdoorModal.js"></script>
* <script>
* CasdoorModal.show({
* serverUrl: "https://your-casdoor-server",
* clientId: "your-client-id",
* redirectUri: "https://your-app.com/callback",
* scope: "read",
* onSuccess: function(data) {
* // data.code, data.state
* console.log("Logged in:", data);
* },
* onClose: function() {
* console.log("Modal closed");
* }
* });
* </script>
*/
(function(window) {
"use strict";
var overlayId = "casdoor-modal-overlay";
function generateState() {
var arr = new Uint8Array(16);
if (window.crypto && window.crypto.getRandomValues) {
window.crypto.getRandomValues(arr);
} else {
for (var i = 0; i < arr.length; i++) {
arr[i] = Math.floor(Math.random() * 256);
}
}
return Array.from(arr).map(function(b) {
return b.toString(16).padStart(2, "0");
}).join("");
}
function buildLoginUrl(options) {
var serverUrl = (options.serverUrl || "").replace(/\/$/, "");
var params = [
"response_type=code",
"client_id=" + encodeURIComponent(options.clientId || ""),
"redirect_uri=" + encodeURIComponent(options.redirectUri || window.location.origin),
"scope=" + encodeURIComponent(options.scope || "read"),
"state=" + encodeURIComponent(options.state || generateState()),
"popup=2",
];
return serverUrl + "/login/oauth/authorize?" + params.join("&");
}
function createStyles() {
var styleId = "casdoor-modal-styles";
if (document.getElementById(styleId)) {
return;
}
var style = document.createElement("style");
style.id = styleId;
style.textContent = [
"#" + overlayId + " {",
" position: fixed;",
" top: 0;",
" left: 0;",
" width: 100%;",
" height: 100%;",
" background: rgba(0, 0, 0, 0.5);",
" display: flex;",
" flex-direction: column;",
" align-items: center;",
" justify-content: center;",
" z-index: 2147483647;",
" box-sizing: border-box;",
" padding: 16px;",
"}",
"#" + overlayId + " .casdoor-modal-container {",
" background: #fff;",
" border-radius: 8px;",
" overflow: hidden;",
" box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);",
" width: 420px;",
" max-width: 100%;",
" max-height: 85vh;",
" overflow-y: auto;",
"}",
"#" + overlayId + " .casdoor-modal-iframe {",
" width: 100%;",
" height: 600px;",
" border: none;",
" display: block;",
"}",
"#" + overlayId + " .casdoor-modal-close-btn {",
" margin-top: 16px;",
" padding: 8px 20px;",
" background: #fff;",
" border: 1px solid #d9d9d9;",
" border-radius: 20px;",
" cursor: pointer;",
" font-size: 14px;",
" color: #555;",
" font-family: sans-serif;",
" line-height: 1.5;",
" transition: background 0.2s, border-color 0.2s;",
" flex-shrink: 0;",
"}",
"#" + overlayId + " .casdoor-modal-close-btn:hover {",
" background: #f5f5f5;",
" border-color: #aaa;",
"}",
].join("\n");
document.head.appendChild(style);
}
var CasdoorModal = {
_overlay: null,
_iframe: null,
_onSuccess: null,
_onClose: null,
_serverOrigin: null,
_messageHandler: null,
/**
* Show the Casdoor login modal.
*
* @param {Object} options
* @param {string} options.serverUrl - Base URL of the Casdoor server (e.g. "https://door.casdoor.com").
* @param {string} options.clientId - OAuth client_id of your application.
* @param {string} [options.redirectUri] - OAuth redirect_uri. Defaults to window.location.origin.
* @param {string} [options.scope] - OAuth scope. Defaults to "read".
* @param {string} [options.state] - OAuth state. Auto-generated if omitted.
* @param {Function} [options.onSuccess] - Callback invoked on successful login with {code, state}.
* @param {Function} [options.onClose] - Callback invoked when the modal is closed without login.
*/
show: function(options) {
if (!options || !options.serverUrl || !options.clientId) {
throw new Error("CasdoorModal.show: options.serverUrl and options.clientId are required.");
}
// Close any existing modal first
if (this._overlay) {
this._removeModal(false);
}
this._onSuccess = options.onSuccess || null;
this._onClose = options.onClose || null;
try {
this._serverOrigin = new URL(options.serverUrl).origin;
} catch (e) {
this._serverOrigin = null;
}
createStyles();
var loginUrl = buildLoginUrl(options);
this._createModal(loginUrl);
this._addMessageListener();
},
/**
* Programmatically close the modal (triggers onClose callback).
*/
close: function() {
if (this._overlay) {
this._removeModal(true);
}
},
_createModal: function(loginUrl) {
var self = this;
var overlay = document.createElement("div");
overlay.id = overlayId;
var container = document.createElement("div");
container.className = "casdoor-modal-container";
var iframe = document.createElement("iframe");
iframe.src = loginUrl;
iframe.className = "casdoor-modal-iframe";
iframe.allow = "publickey-credentials-get *";
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-top-navigation-by-user-activation");
container.appendChild(iframe);
overlay.appendChild(container);
var closeBtn = document.createElement("button");
closeBtn.className = "casdoor-modal-close-btn";
closeBtn.innerHTML = "&#x2715;&nbsp;Close";
closeBtn.onclick = function() {
self.close();
};
overlay.appendChild(closeBtn);
overlay.addEventListener("click", function(e) {
if (e.target === overlay) {
self.close();
}
});
document.body.appendChild(overlay);
this._overlay = overlay;
this._iframe = iframe;
},
_removeModal: function(fireCloseCallback) {
this._removeMessageListener();
if (this._overlay && this._overlay.parentNode) {
this._overlay.parentNode.removeChild(this._overlay);
}
this._overlay = null;
this._iframe = null;
if (fireCloseCallback && this._onClose) {
var cb = this._onClose;
this._onClose = null;
cb();
} else {
this._onClose = null;
}
this._onSuccess = null;
},
_addMessageListener: function() {
var self = this;
this._messageHandler = function(event) {
// Validate origin against the configured server URL
if (self._serverOrigin && event.origin !== self._serverOrigin) {
return;
}
var data = event.data;
if (!data || typeof data !== "object") {
return;
}
if (data.type === "loginSuccess") {
var successCb = self._onSuccess;
self._removeModal(false);
if (successCb) {
successCb(data.data);
}
} else if (data.type === "windowClosed") {
self._removeModal(true);
}
};
window.addEventListener("message", this._messageHandler);
},
_removeMessageListener: function() {
if (this._messageHandler) {
window.removeEventListener("message", this._messageHandler);
this._messageHandler = null;
}
},
};
window.CasdoorModal = CasdoorModal;
})(window);

View File

@@ -129,7 +129,7 @@ class LoginPage extends React.Component {
this.login(values);
}
if (params.get("popup") === "1") {
if (params.get("popup") === "1" || params.get("popup") === "2") {
window.addEventListener("beforeunload", () => {
this.sendPopupData({type: "windowClosed"}, params.get("redirect_uri"));
});
@@ -345,8 +345,11 @@ class LoginPage extends React.Component {
sendPopupData(message, redirectUri) {
const params = new URLSearchParams(this.props.location.search);
if (params.get("popup") === "1") {
const popup = params.get("popup");
if (popup === "1") {
window.opener.postMessage(message, redirectUri);
} else if (popup === "2") {
window.parent.postMessage(message, redirectUri || "*");
}
}
@@ -405,7 +408,10 @@ class LoginPage extends React.Component {
}, 1000);
}
} else {
Setting.goToLink(redirectUrl);
const popup = new URLSearchParams(this.props.location.search).get("popup");
if (popup !== "2") {
Setting.goToLink(redirectUrl);
}
this.sendPopupData({type: "loginSuccess", data: {code: code, state: oAuthParams.state}}, oAuthParams.redirectUri);
}
}