bump: 4.0 - new react frontend

This commit is contained in:
2026-03-15 17:27:16 +01:00
parent 6438deaa4e
commit becc4f9445
24 changed files with 722 additions and 262 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,25 +0,0 @@
<!DOCTYPE html>
<html lang="es" class="justify-content-center">
<head>
<meta charset="UTF-8">
<title>Autofill</title>
<link rel="stylesheet" href="./css/styles.css">
<link href="./css/bootstrap.min.css" rel="stylesheet">
<script src="./scripts/bootstrap.bundle.min.js"></script>
<script src="./scripts/bootstrap.min.js"></script>
</head>
<body class="bg-dark border-us p-4">
<div class="bg-dark container justify-content-center row text-white p-0 m-0">
<h4 class="display-6 mb-1 p-0 text-center">US 2FA Autofill</h4>
<small class="mb-4 p-0 text-center">Para obtener tu secret ve <a href="https://2fa.us.es/a2f/otp/bind-device" target="_blank">aquí</a><br>y dale a 'Mostrar' -> 'Mostrar parámetros de configuración'</small>
<input class="mb-2" id="2fa-input" type="text" name="secret" autocomplete="off" placeholder="Introduce tu secret SHA-1">
<p class="item2fa m-0 col-sm-7 mb-2 p-0 m-0 text-center display" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Copiar" id="codigo2FA">000000</p>
<div class="counter bg-us mt-2 mb-4" id="counter">Reinicio en: 30s</div>
<div class="counter bg-us text-white" id="reset">Restablecer secret</div>
</div>
<script src="./scripts/scripts.js"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -1,57 +0,0 @@
html {
width: 400px;
max-width: 400px;
}
.bg-us {
background-color: #be0f2e;
}
.border-us {
border: 3px solid #be0f2e;
}
.copy {
font-size: 36px;
text-align: center;
width: 36px;
height: 36px;
}
.counter {
text-align: center;
background-color: #be0f2e;
font-size: 24px;
color: #ffffff;
border-radius: px;
}
input {
width: 100%;
margin: 8px 0;
padding: 6px 12px 6px 12px;
display: inline-block;
border: 1px solid #be0f2e;
border-radius: 4px;
background-color: #111;
margin-top: 20px;
caret-color: white;
color: white;
box-sizing: border-box;
}
#codigo2FA {
font-size: 48px;
padding: 0%;
}
#codigo2FA:hover {
cursor: pointer;
text-decoration: underline;
font-weight: bold;
}
#reset:hover {
cursor: pointer;
background-color: #940f27;
}

29
eslint.config.js Normal file
View File

@@ -0,0 +1,29 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])

11
index.html Normal file
View File

@@ -0,0 +1,11 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>US 2FA Autofill</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

9
jsconfig.json Normal file
View File

@@ -0,0 +1,9 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"]
}

View File

@@ -1,30 +0,0 @@
{
"manifest_version": 3,
"name": "US 2FA Autofill",
"description": "Rellena el código de 2FA en los sitios de la US",
"version": "3.0",
"icons": {
"16": "images/USAutofill_16.png",
"128": "images/USAutofill_128.png"
},
"action": {
"default_popup": "autofill.html",
"default_icon": "images/USAutofill_128.png"
},
"permissions": [
"activeTab",
"tabs",
"scripting",
"storage"
],
"content_scripts": [
{
"matches": [
"https://sso.us.es/*"
],
"js": [
"scripts/scripts.js"
]
}
]
}

33
package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "us-2fa-react",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^7.2.0",
"@fortawesome/free-solid-svg-icons": "^7.2.0",
"@fortawesome/react-fontawesome": "^3.2.0",
"bootstrap": "^5.3.8",
"otpauth": "^9.5.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"vite-plugin-clean": "^2.0.1"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.0",
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"vite": "^8.0.0"
}
}

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 536 B

After

Width:  |  Height:  |  Size: 536 B

43
public/manifest.json Normal file
View File

@@ -0,0 +1,43 @@
{
"manifest_version": 3,
"name": "US 2FA Autofill",
"version": "4.0",
"description": "Rellena automáticamente el código 2FA en la Universidad de Sevilla.",
"permissions": [
"activeTab",
"scripting",
"tabs",
"storage"
],
"action": {
"default_popup": "index.html"
},
"icons": {
"128": "images/USAutofill_128.png"
},
"content_scripts": [
{
"matches": [
"https://sso.us.es/*"
],
"js": [
"scripts/content.js"
],
"run_at": "document_idle"
}
],
"host_permissions": [
"https://sso.us.es/*"
],
"browser_specific_settings": {
"gecko": {
"id": "{552d76df-199a-4058-a8ef-77478cad362f}",
"strict_min_version": "142.0",
"data_collection_permissions": {
"required": [
"none"
]
}
}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

145
src/App.jsx Normal file
View File

@@ -0,0 +1,145 @@
import { useState, useEffect } from "react";
import * as OTPAuth from 'otpauth';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { ThemeToggle } from "./components/ThemeToggle";
const App = () => {
const [secret, setSecret] = useState(localStorage.getItem('shasecret') || '');
const [token, setToken] = useState('000000');
const [remaining, setRemaining] = useState(30);
useEffect(() => {
if (!secret) return;
const generateToken = () => {
try {
let totp = new OTPAuth.TOTP({
issuer: 'US',
label: '2FA',
algorithm: 'SHA1',
digits: 6,
period: 30,
secret: OTPAuth.Secret.fromBase32(secret.trim().toUpperCase()),
});
const newToken = totp.generate()
setToken(newToken);
// eslint-disable-next-line no-undef
const api = typeof browser !== "undefined" ? browser : chrome;
if (api && api.tabs && api.tabs.query) {
api.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs[0]?.id && tabs[0].url?.includes("sso.us.es")) {
api.tabs.sendMessage(tabs[0].id, {
action: "autofill",
code: newToken
}).catch(() => {
console.log("Esperando inyección...");
});
}
});
} else {
console.log("Entorno de desarrollo: Autofill desactivado.");
}
} catch (err) {
console.error("Invalid secret:", err);
setToken("ERR!");
}
};
generateToken();
const timer = setInterval(() => {
const now = Date.now() / 1000;
const secondsInCycle = now % 30;
const remainingTime = 30 - secondsInCycle;
setRemaining(remainingTime);
if (remainingTime > 29.9) generateToken();
}, 41); // ~41.66ms = 24fps
return () => clearInterval(timer);
}, [secret]);
const handleSaveSecret = (e) => {
if (e.key === 'Enter') {
const val = e.target.value.trim();
localStorage.setItem('shasecret', val);
setSecret(val);
e.target.value = '';
}
};
const copyToClipboard = () => {
navigator.clipboard.writeText(token);
};
const resetSecret = () => {
localStorage.removeItem('shasecret');
setSecret('');
setToken('000000');
};
return (
<div className="container-app">
<div className="top-actions">
{!secret ? (
<div className="tooltip-container left">
<FontAwesomeIcon icon={faCircleInfo} className="info-icon" />
<div className="tooltip-text">
Para obtener tu secret ve <a href="https://2fa.us.es/a2f/otp/bind-device" target="_blank">aquí</a>
&nbsp;y dale a 'Mostrar' -&gt; 'Mostrar parámetros de configuración'
</div>
</div>
) : (
<div />
)}
<ThemeToggle className="right" />
</div>
{!secret && (
<div className="header-wrapper">
<h3>US 2FA Autofill</h3>
</div>
)}
{!secret ? (
<div className="setup-view">
<span className="info-text">
Mete tu secret SHA-1 de la US y pulsa Enter
</span>
<input
type="text"
onKeyUp={handleSaveSecret}
placeholder="Introduce secret..."
/>
</div>
) : (
<div className="token-view">
<div
id="codigo2FA"
onClick={copyToClipboard}
style={{
"--progress-smooth": `${(remaining / 30) * 100}%`
}}
>
{token}
</div>
<button className="btn-reset" onClick={resetSecret}>
Cambiar Secret
</button>
</div>
)}
<footer className="app-footer">
<small>Dev'd with &lt;3 by <a className="text-decoration-none" target="_blank" href="https://jose.miarma.net"><strong>Gallardo7761</strong></a></small>
</footer>
</div>
);
}
export default App;

View File

@@ -0,0 +1,20 @@
import { useTheme } from "@/hooks/useTheme";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons";
import '@/css/ThemeToggle.css';
const ThemeToggle = ({className}) => {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
className={`theme-btn m-0 p-0 ` + className}
title={theme === "light" ? "flashbang!" : "go to sleep"}
>
<FontAwesomeIcon icon={theme === "light" ? faMoon : faSun} />
</button>
);
};
export {ThemeToggle};

25
src/content.js Normal file
View File

@@ -0,0 +1,25 @@
// eslint-disable-next-line no-undef
const api = typeof browser !== "undefined" ? browser : chrome;
api.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === "autofill") {
const input = document.getElementById('input2factor');
const button = document.getElementById('btn-login');
if (input) {
input.focus();
input.value = request.code;
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
if (button) {
setTimeout(() => {
button.click();
}, 150);
}
sendResponse({ status: "bomba" });
}
}
return true;
});

View File

@@ -0,0 +1,31 @@
import { createContext, useEffect, useState } from "react";
export const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState(() => {
return (
localStorage.getItem("theme") ||
(window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
);
});
useEffect(() => {
const root = document.documentElement;
document.body.classList.remove("light", "dark");
document.body.classList.add(theme);
root.classList.remove("light", "dark");
root.classList.add(theme);
localStorage.setItem("theme", theme);
}, [theme]);
const toggleTheme = () => {
setTheme((prev) => (prev === "light" ? "dark" : "light"));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};

17
src/css/ThemeToggle.css Normal file
View File

@@ -0,0 +1,17 @@
.theme-btn {
background: transparent;
border: none;
color: var(--text-muted);
font-size: 1.2rem;
cursor: pointer;
padding: 5px;
transition: color 0.2s, transform 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.theme-btn:hover {
color: var(--primary);
transform: rotate(15deg); /* Un detallito con arte */
}

277
src/css/index.css Normal file
View File

@@ -0,0 +1,277 @@
/* VARIABLES */
:root {
--primary: #be0f2e;
--primary-hover: #940f27;
--transition-speed: 0.3s;
}
:root.light {
--bg-body: #f8f9fa;
--card-bg: #ffffff;
--text-main: #212529;
--text-muted: #6c757d;
--input-bg: #ffffff;
}
:root.dark {
--bg-body: #121212;
--card-bg: #1e1e1e;
--text-main: #e9ecef;
--text-muted: #adb5bd;
--input-bg: #2d2d2d;
}
/* BASE */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
width: 360px;
margin: 0px;
padding: 0px;
}
body {
color: var(--text-main);
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
transition: all var(--transition-speed) ease;
display: flex;
justify-content: center;
padding: 0px;
margin: 0px;
}
/* LAYOUT */
.container-app {
width: 360px;
position: relative;
background: var(--card-bg);
padding: 30px 25px 15px 25px;
border: 1px solid rgba(190, 15, 46, 0.2);
}
.top-actions {
position: absolute;
top: 15px;
left: 0;
right: 0;
padding: 10px 20px;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 10;
pointer-events: none;
}
.top-actions > * {
pointer-events: auto;
}
.header-wrapper {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin-bottom: 20px;
}
/* TIPOGRAFÍA */
h3, .header-wrapper h3 {
font-weight: 700;
text-align: center;
margin: 0;
margin-bottom: 10px;
color: var(--primary);
line-height: 1;
}
.token-header {
text-align: center;
font-size: 1.1rem;
color: var(--text-muted);
margin-bottom: -10px;
font-weight: 500;
}
.info-text {
display: block;
font-size: 0.85rem;
color: var(--text-muted);
text-align: center;
line-height: 1.4;
margin-bottom: 5px;
}
.info-text a {
color: var(--primary);
text-decoration: none;
font-weight: 600;
}
.info-text a:hover {
text-decoration: underline;
}
.muted {
color: var(--text-muted);
}
/* COMPONENTES */
input {
width: 100%;
padding: 12px;
border: 2px solid #ddd;
border-radius: 8px;
background: var(--input-bg);
color: var(--text-main);
font-size: 0.9rem;
outline: none;
transition: border-color 0.2s;
}
:root.dark input {
border-color: #444;
}
input:focus {
border-color: var(--primary);
}
#codigo2FA {
font-family: 'Courier New', Courier, monospace;
font-size: 3.5rem;
font-weight: 800;
text-align: center;
padding: 10px 0;
margin: 5px 0;
letter-spacing: 5px;
cursor: pointer;
user-select: none;
display: inline-block;
width: 100%;
background: linear-gradient(
to right,
var(--primary) calc(var(--progress-smooth) * 0.7 + 15%),
var(--text-muted) calc(var(--progress-smooth) * 0.7 + 15%)
);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
transition: all 0.1s linear;
}
#codigo2FA:hover {
transform: scale(1.05);
transition: 0.2s;
filter: brightness(1.2);
}
.btn-reset {
background: transparent;
color: var(--text-muted);
border: none;
width: 100%;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
}
.btn-reset::before {
content: '[';
}
.btn-reset::after {
content: ']';
}
.btn-reset:hover {
color: var(--primary);
}
/* TOOLTIPS */
.tooltip-container {
position: relative;
display: flex;
align-items: center;
}
.info-icon {
color: var(--text-muted);
font-size: 1.1rem;
transition: color 0.2s;
}
.tooltip-text {
visibility: hidden;
width: 280px;
background-color: var(--card-bg);
color: var(--text-main);
text-align: center;
border-radius: 8px;
padding: 12px;
position: absolute;
z-index: 9999;
top: 140%;
right: -10px;
opacity: 0;
transition: opacity 0.2s ease-in-out;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
border: 1.5px solid var(--primary);
font-size: 0.85rem;
line-height: 1.4;
pointer-events: auto;
word-wrap: break-word;
}
.tooltip-text::after {
content: "";
position: absolute;
bottom: 100%;
right: 15px;
border-width: 7px;
border-style: solid;
border-color: transparent transparent var(--primary) transparent;
}
.top-actions .left .tooltip-text {
left: -10px;
right: auto;
}
.top-actions .left .tooltip-text::after {
left: 15px;
right: auto;
}
.tooltip-container:hover .tooltip-text {
visibility: visible;
opacity: 1;
}
/* FOOTER */
.app-footer {
margin-top: 20px;
text-align: center;
opacity: 0.6;
transition: opacity 0.3s;
}
.app-footer:hover {
opacity: 1;
}
.app-footer small {
font-size: 0.8rem;
color: var(--text-muted);
letter-spacing: 0.5px;
}
.app-footer strong {
color: var(--primary);
}

10
src/hooks/useTheme.js Normal file
View File

@@ -0,0 +1,10 @@
import { useContext } from "react";
import { ThemeContext } from "../context/ThemeContext";
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme debe usarse dentro de un <ThemeProvider>");
}
return context;
};

15
src/main.jsx Normal file
View File

@@ -0,0 +1,15 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import '@/css/index.css'
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap/dist/js/bootstrap.bundle.min.js'
import App from '@/App.jsx'
import { ThemeProvider } from '@/context/ThemeContext'
createRoot(document.getElementById('root')).render(
<StrictMode>
<ThemeProvider>
<App />
</ThemeProvider>
</StrictMode>,
)

33
vite.config.js Normal file
View File

@@ -0,0 +1,33 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
input: [
resolve(__dirname, 'index.html'),
resolve(__dirname, 'src/content.js')
],
output: {
entryFileNames: (chunkInfo) => {
if (chunkInfo.name === 'content') {
return 'scripts/content.js';
}
return 'assets/[name]-[hash].js';
},
},
},
},
server: {
port: 3000,
},
resolve: {
alias: {
'@/': '/src/',
},
},
publicDir: 'public',
});