diff --git a/otoroshi/app/otoroshi/views/backoffice/unauthmain.scala.html b/otoroshi/app/otoroshi/views/backoffice/unauthmain.scala.html
index 0ee1aaf6c7..f4d70d7e24 100644
--- a/otoroshi/app/otoroshi/views/backoffice/unauthmain.scala.html
+++ b/otoroshi/app/otoroshi/views/backoffice/unauthmain.scala.html
@@ -6,12 +6,34 @@
diff --git a/otoroshi/javascript/src/pages/U2FLoginPage.js b/otoroshi/javascript/src/pages/U2FLoginPage.js
index 5311cb2a3f..a99f0e23bc 100644
--- a/otoroshi/javascript/src/pages/U2FLoginPage.js
+++ b/otoroshi/javascript/src/pages/U2FLoginPage.js
@@ -1,7 +1,8 @@
-import React, { Component } from 'react';
+import React, { Component } from "react";
function Base64Url() {
- let chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
+ let chars =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
// Use a lookup table to find the index.
let lookup = new Uint8Array(256);
@@ -13,7 +14,7 @@ function Base64Url() {
let bytes = new Uint8Array(arraybuffer),
i,
len = bytes.length,
- base64url = '';
+ base64url = "";
for (i = 0; i < len; i += 3) {
base64url += chars[bytes[i] >> 2];
@@ -76,7 +77,7 @@ function responseToObject(response) {
try {
clientExtensionResults = response.getClientExtensionResults();
} catch (e) {
- console.error('getClientExtensionResults failed', e);
+ console.error("getClientExtensionResults failed", e);
}
if (response.response.attestationObject) {
@@ -84,8 +85,12 @@ function responseToObject(response) {
type: response.type,
id: response.id,
response: {
- attestationObject: base64url.fromByteArray(response.response.attestationObject),
- clientDataJSON: base64url.fromByteArray(response.response.clientDataJSON),
+ attestationObject: base64url.fromByteArray(
+ response.response.attestationObject
+ ),
+ clientDataJSON: base64url.fromByteArray(
+ response.response.clientDataJSON
+ ),
},
clientExtensionResults,
};
@@ -94,11 +99,16 @@ function responseToObject(response) {
type: response.type,
id: response.id,
response: {
- authenticatorData: base64url.fromByteArray(response.response.authenticatorData),
- clientDataJSON: base64url.fromByteArray(response.response.clientDataJSON),
+ authenticatorData: base64url.fromByteArray(
+ response.response.authenticatorData
+ ),
+ clientDataJSON: base64url.fromByteArray(
+ response.response.clientDataJSON
+ ),
signature: base64url.fromByteArray(response.response.signature),
userHandle:
- response.response.userHandle && base64url.fromByteArray(response.response.userHandle),
+ response.response.userHandle &&
+ base64url.fromByteArray(response.response.userHandle),
},
clientExtensionResults,
};
@@ -106,22 +116,271 @@ function responseToObject(response) {
}
}
+// Inject styles for animations
+const injectStyles = () => {
+ if (document.getElementById("otoroshi-admin-login-styles")) return;
+ const style = document.createElement("style");
+ style.id = "otoroshi-admin-login-styles";
+ style.textContent = `
+ @keyframes oto-fade-in {
+ from { opacity: 0; transform: translateY(8px); }
+ to { opacity: 1; transform: translateY(0); }
+ }
+ @keyframes oto-glow-pulse {
+ 0%, 100% { opacity: 0.4; }
+ 50% { opacity: 0.7; }
+ }
+ @keyframes oto-spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+ }
+ .oto-login-input::placeholder {
+ color: rgba(255, 255, 255, 0.3);
+ }
+ .oto-login-input:focus {
+ border-color: #f9b000;
+ box-shadow: 0 0 0 1px #f9b000, 0 0 16px rgba(249, 176, 0, 0.12);
+ outline: none;
+ }
+ .oto-login-btn:hover:not(:disabled) {
+ background: #e6a200;
+ box-shadow: 0 8px 24px rgba(249, 176, 0, 0.3);
+ transform: translateY(-1px);
+ }
+ .oto-login-btn:active:not(:disabled) {
+ transform: translateY(0);
+ }
+ .oto-login-btn:disabled {
+ opacity: 0.7;
+ cursor: not-allowed;
+ }
+ .oto-webauthn-btn:hover:not(:disabled) {
+ background: rgba(255, 255, 255, 0.08);
+ border-color: rgba(255, 255, 255, 0.35);
+ }
+ .oto-footer-link:hover {
+ color: #f9b000;
+ }
+ `;
+ document.head.appendChild(style);
+};
+
+const styles = {
+ container: {
+ minHeight: "100vh",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ background: "linear-gradient(135deg, #1b1b2f 0%, #162447 50%, #1f4068 100%)",
+ fontFamily:
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif',
+ padding: "24px",
+ position: "relative",
+ overflow: "hidden",
+ },
+ backgroundOrb: {
+ position: "absolute",
+ top: "10%",
+ left: "50%",
+ transform: "translateX(-50%)",
+ width: "600px",
+ height: "600px",
+ background:
+ "radial-gradient(circle, rgba(249, 176, 0, 0.06) 0%, transparent 70%)",
+ pointerEvents: "none",
+ animation: "oto-glow-pulse 4s ease-in-out infinite",
+ },
+ content: {
+ position: "relative",
+ zIndex: 1,
+ width: "100%",
+ maxWidth: "400px",
+ animation: "oto-fade-in 0.5s ease-out",
+ },
+ brandSection: {
+ textAlign: "center",
+ marginBottom: "32px",
+ },
+ logo: {
+ height: "120px",
+ width: "auto",
+ },
+ card: {
+ background: "rgba(0, 0, 0, 0.35)",
+ backdropFilter: "blur(20px)",
+ border: "1px solid rgba(255, 255, 255, 0.1)",
+ borderRadius: "12px",
+ padding: "32px",
+ boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.4)",
+ },
+ cardHeader: {
+ marginBottom: "28px",
+ textAlign: "center",
+ },
+ title: {
+ margin: "0 0 8px",
+ color: "#fff",
+ fontSize: "24px",
+ fontWeight: "700",
+ letterSpacing: "-0.02em",
+ },
+ subtitle: {
+ margin: 0,
+ color: "rgba(255, 255, 255, 0.6)",
+ fontSize: "14px",
+ lineHeight: "1.5",
+ },
+ form: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "20px",
+ },
+ inputGroup: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "8px",
+ },
+ label: {
+ color: "rgba(255, 255, 255, 0.7)",
+ fontSize: "13px",
+ fontWeight: "500",
+ },
+ input: {
+ width: "100%",
+ padding: "12px 14px",
+ fontSize: "14px",
+ color: "#fff",
+ backgroundColor: "rgba(0, 0, 0, 0.3)",
+ border: "1px solid rgba(255, 255, 255, 0.12)",
+ borderRadius: "8px",
+ outline: "none",
+ transition: "all 0.2s ease",
+ boxSizing: "border-box",
+ },
+ errorContainer: {
+ display: "flex",
+ alignItems: "center",
+ gap: "8px",
+ padding: "12px 14px",
+ backgroundColor: "rgba(220, 38, 38, 0.1)",
+ border: "1px solid rgba(220, 38, 38, 0.2)",
+ borderRadius: "8px",
+ color: "#fca5a5",
+ fontSize: "13px",
+ },
+ errorIcon: {
+ width: "16px",
+ height: "16px",
+ flexShrink: 0,
+ color: "#f87171",
+ },
+ successContainer: {
+ display: "flex",
+ alignItems: "center",
+ gap: "8px",
+ padding: "12px 14px",
+ backgroundColor: "rgba(16, 185, 129, 0.1)",
+ border: "1px solid rgba(16, 185, 129, 0.2)",
+ borderRadius: "8px",
+ color: "#6ee7b7",
+ fontSize: "13px",
+ },
+ successIcon: {
+ width: "16px",
+ height: "16px",
+ flexShrink: 0,
+ color: "#10b981",
+ },
+ buttonGroup: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "12px",
+ marginTop: "4px",
+ },
+ button: {
+ width: "100%",
+ padding: "12px 20px",
+ fontSize: "14px",
+ fontWeight: "700",
+ color: "#1b1b1d",
+ background: "#f9b000",
+ border: "none",
+ borderRadius: "8px",
+ cursor: "pointer",
+ transition: "all 0.2s ease",
+ boxShadow: "0 4px 14px rgba(249, 176, 0, 0.2)",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ gap: "8px",
+ },
+ webauthnButton: {
+ width: "100%",
+ padding: "12px 20px",
+ fontSize: "14px",
+ fontWeight: "500",
+ color: "rgba(255, 255, 255, 0.7)",
+ background: "transparent",
+ border: "1px solid rgba(255, 255, 255, 0.2)",
+ borderRadius: "8px",
+ cursor: "pointer",
+ transition: "all 0.2s ease",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ gap: "8px",
+ },
+ spinner: {
+ width: "18px",
+ height: "18px",
+ animation: "oto-spin 1s linear infinite",
+ },
+ spinnerTrack: {
+ opacity: 0.2,
+ },
+ spinnerHead: {
+ opacity: 1,
+ },
+ footer: {
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ gap: "8px",
+ marginTop: "32px",
+ color: "rgba(255, 255, 255, 0.35)",
+ fontSize: "12px",
+ },
+ footerLink: {
+ color: "rgba(255, 255, 255, 0.5)",
+ textDecoration: "none",
+ transition: "color 0.2s ease",
+ },
+};
+
export class U2FLoginPage extends Component {
state = {
- email: '',
- password: '',
+ email: "",
+ password: "",
error: null,
message: null,
+ loading: false,
};
+ componentDidMount() {
+ injectStyles();
+ }
+
onChange = (e) => {
- this.setState({ [e.target.name]: e.target.value });
+ this.setState({ [e.target.name]: e.target.value, error: null });
};
handleError = (mess, t) => {
return (err) => {
console.log(err && err.message ? err.message : err);
- this.setState({ error: err && err.message ? err.message : err });
+ this.setState({
+ error: err && err.message ? err.message : err,
+ loading: false,
+ });
throw err;
};
};
@@ -132,13 +391,13 @@ export class U2FLoginPage extends Component {
}
const username = this.state.email;
const password = this.state.password;
- this.setState({ message: null });
+ this.setState({ message: null, loading: true, error: null });
return fetch(`/bo/simple/login`, {
- method: 'POST',
- credentials: 'include',
+ method: "POST",
+ credentials: "include",
headers: {
- Accept: 'application/json',
- 'Content-Type': 'application/json',
+ Accept: "application/json",
+ "Content-Type": "application/json",
},
body: JSON.stringify({
username,
@@ -146,11 +405,12 @@ export class U2FLoginPage extends Component {
}),
}).then((r) => {
if (r && r.ok) {
- window.location.href = '/bo/dashboard';
+ this.setState({ message: "Login successful, redirecting..." });
+ window.location.href = "/bo/dashboard";
} else {
this.webAuthnLogin();
}
- }, this.handleError('Login and/or password error, sorry ...'));
+ }, this.handleError("Invalid username or password"));
};
webAuthnLogin = (e) => {
@@ -160,13 +420,17 @@ export class U2FLoginPage extends Component {
const username = this.state.email;
const password = this.state.password;
const label = this.state.label;
- this.setState({ message: null });
+ this.setState({
+ message: "Waiting for security key...",
+ loading: true,
+ error: null,
+ });
fetch(`/bo/webauthn/login/start`, {
- method: 'POST',
- credentials: 'include',
+ method: "POST",
+ credentials: "include",
headers: {
- Accept: 'application/json',
- 'Content-Type': 'application/json',
+ Accept: "application/json",
+ "Content-Type": "application/json",
},
body: JSON.stringify({
username,
@@ -179,9 +443,9 @@ export class U2FLoginPage extends Component {
if (r.status === 200 || r.status == 201) {
return r.json();
} else {
- throw new Error('Login and/or password error, sorry ...');
+ throw new Error("Invalid username or password");
}
- }, this.handleError('Login and/or password error, sorry ...'))
+ }, this.handleError("Invalid username or password"))
.then((payload) => {
const requestId = payload.requestId;
const options = payload.request.publicKeyCredentialRequestOptions;
@@ -190,22 +454,21 @@ export class U2FLoginPage extends Component {
c.id = base64url.decode(c.id);
return c;
});
- console.log(options);
return navigator.credentials
.get(
{
publicKey: options,
},
- this.handleError('Webauthn error, sorry ...')
+ this.handleError("WebAuthn authentication failed")
)
.then((credentials) => {
const json = responseToObject(credentials);
return fetch(`/bo/webauthn/login/finish`, {
- method: 'POST',
- credentials: 'include',
+ method: "POST",
+ credentials: "include",
headers: {
- Accept: 'application/json',
- 'Content-Type': 'application/json',
+ Accept: "application/json",
+ "Content-Type": "application/json",
},
body: JSON.stringify({
requestId,
@@ -217,86 +480,187 @@ export class U2FLoginPage extends Component {
},
}),
})
- .then((r) => r.json(), this.handleError('Authentication error, sorry ...'))
+ .then((r) => r.json(), this.handleError("Authentication failed"))
.then((data) => {
this.setState(
- { error: null, email: '', password: '', message: `Login successfully` },
+ {
+ error: null,
+ email: "",
+ password: "",
+ message: "Login successful, redirecting...",
+ loading: false,
+ },
() => {
- window.location.href = '/bo/dashboard';
+ window.location.href = "/bo/dashboard";
}
);
- }, this.handleError('Login error, sorry ...'));
+ }, this.handleError("Login failed"));
});
- }, this.handleError('Login error, sorry ...'));
+ }, this.handleError("Authentication failed"));
};
render() {
+ const { email, password, error, message, loading } = this.state;
+ const canSubmit = email && password && !loading;
+
return (
-
-

-
-
Admin login
-
Log in to Otoroshi to continue
-
-