191
README.md
191
README.md
@@ -1,3 +1,190 @@
|
|||||||
# myip – IP & RIPE Lookup (extended)
|
# 🌐 myip – IP & RIPE Lookup WebApp
|
||||||
|
|
||||||
UI moderna + IP reale + RIPE esteso (ASN, Provider, Prefix, Routing Status, Reverse DNS, GeoLocation, Abuse Contact).
|
myip è una webapp minimale e containerizzata che mostra:
|
||||||
|
|
||||||
|
- il **tuo IP pubblico**
|
||||||
|
- informazioni RIPE utili (ASN, Provider/Holder, Prefisso annunciato)
|
||||||
|
- UI moderna e responsiva
|
||||||
|
- pulsante **Copia IP**
|
||||||
|
- integrazione con ingress-nginx e PROXY protocol
|
||||||
|
- design personalizzabile tramite CSS dedicato
|
||||||
|
|
||||||
|
Pensata per essere deployata come micro-servizio in Kubernetes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Screenshot
|
||||||
|
|
||||||
|
> 
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Funzionalità
|
||||||
|
|
||||||
|
### 🔍 Identificazione del client
|
||||||
|
- Recupero del **vero IP del client** anche dietro più proxy/load balancer.
|
||||||
|
- Compatibile con:
|
||||||
|
- `X-Forwarded-For`
|
||||||
|
- `X-Real-IP`
|
||||||
|
- PROXY protocol v2
|
||||||
|
|
||||||
|
### 🛰️ Lookup RIPEstat
|
||||||
|
Per l’indirizzo IP viene mostrato:
|
||||||
|
- ASN
|
||||||
|
- Provider (holder)
|
||||||
|
- Prefisso annunciato
|
||||||
|
|
||||||
|
### 🎨 UI moderna
|
||||||
|
- Font Montserrat
|
||||||
|
- Layout centrato
|
||||||
|
- Logo cliccabile
|
||||||
|
- Animazioni CSS
|
||||||
|
- Copy-to-clipboard
|
||||||
|
- Modalità dark automatica
|
||||||
|
|
||||||
|
### 🔧 Semplice da deployare
|
||||||
|
- Dockerfile incluso
|
||||||
|
- Configurazione Helm-ready
|
||||||
|
- Compatibile con ingress-nginx e cert-manager
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 Struttura del progetto
|
||||||
|
|
||||||
|
```
|
||||||
|
myip-webapp/
|
||||||
|
├─ app.py
|
||||||
|
├─ requirements.txt
|
||||||
|
├─ Dockerfile
|
||||||
|
├─ static/
|
||||||
|
│ ├─ myip.css
|
||||||
|
│ ├─ logo.png
|
||||||
|
│ └─ favicon.ico
|
||||||
|
└─ README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐳 Deploy con Docker
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t myip:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -p 8080:8000 myip
|
||||||
|
```
|
||||||
|
|
||||||
|
Apri:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ☸️ Deploy in Kubernetes (Helm)
|
||||||
|
|
||||||
|
### values.yaml minimale
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
className: nginx
|
||||||
|
host: myip.example.com
|
||||||
|
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: letsencrypt-production
|
||||||
|
nginx.ingress.kubernetes.io/use-forwarded-headers: "true"
|
||||||
|
nginx.ingress.kubernetes.io/real-ip-header: "X-Forwarded-For"
|
||||||
|
nginx.ingress.kubernetes.io/compute-full-forwarded-for: "true"
|
||||||
|
|
||||||
|
tls:
|
||||||
|
enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### ingress-nginx PROXY protocol (fondamentale)
|
||||||
|
|
||||||
|
Nella ConfigMap del controller:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
data:
|
||||||
|
use-proxy-protocol: "true"
|
||||||
|
real-ip-header: "proxy_protocol"
|
||||||
|
set-real-ip-from: "xxx.xxx.xxx.0/24"
|
||||||
|
use-forwarded-headers: "true"
|
||||||
|
```
|
||||||
|
|
||||||
|
### HAProxy (L4) davanti ad ingress-nginx
|
||||||
|
|
||||||
|
```haproxy
|
||||||
|
server backend1 xxx.xxx.xxx.xxx:443 send-proxy-v2 check
|
||||||
|
server backend2 xxx.xxx.xxx.xxx:443 send-proxy-v2 check
|
||||||
|
server backend3 xxx.xxx.xxx.xxx:443 send-proxy-v2 check
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧠 Note Tecniche
|
||||||
|
|
||||||
|
### Determinazione dell’IP reale
|
||||||
|
|
||||||
|
L’app utilizza:
|
||||||
|
|
||||||
|
```python
|
||||||
|
X-Forwarded-For → X-Real-IP → remote_addr
|
||||||
|
```
|
||||||
|
|
||||||
|
ed è compatibile con proxy multipli e ingress-nginx.
|
||||||
|
|
||||||
|
### Lookup RIPE
|
||||||
|
|
||||||
|
Usa la API ufficiale RIPEstat:
|
||||||
|
|
||||||
|
- `/network-info/`
|
||||||
|
- `/as-overview/`
|
||||||
|
|
||||||
|
Timeout veloce (2s) per non bloccare la UI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Personalizzazioni
|
||||||
|
|
||||||
|
Tutto il design è modificabile in:
|
||||||
|
|
||||||
|
```
|
||||||
|
static/myip.css
|
||||||
|
```
|
||||||
|
|
||||||
|
Puoi sostituire:
|
||||||
|
- `logo.png` → per branding
|
||||||
|
- `favicon.ico` → icona personalizzata
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏁 Roadmap
|
||||||
|
|
||||||
|
- [ ] Endpoint `/api/ip`
|
||||||
|
- [ ] Multi-theme (light/dark manuale)
|
||||||
|
- [ ] Mini-widget JavaScript includibile in altri siti
|
||||||
|
- [ ] Supporto IPv6-only
|
||||||
|
- [ ] Caching locale del lookup RIPE
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 Credits
|
||||||
|
|
||||||
|
- Frontend & Styling by ChatGPT + AB style guidelines
|
||||||
|
- Backend Python + Flask
|
||||||
|
- Lookup dati: **RIPEstat Data API**
|
||||||
|
- Supporto PROXY prot. v2: HAProxy + ingress-nginx
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 Licenza
|
||||||
|
|
||||||
|
MIT (o altra licenza a tua scelta)
|
||||||
|
|||||||
148
app.py
148
app.py
@@ -5,35 +5,31 @@ from werkzeug.middleware.proxy_fix import ProxyFix
|
|||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1)
|
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1)
|
||||||
|
|
||||||
RIPESTAT_BASE = "https://stat.ripe.net/data"
|
|
||||||
|
|
||||||
HTML_TEMPLATE = """<!doctype html>
|
HTML_TEMPLATE = """<!doctype html>
|
||||||
<html lang="it">
|
<html lang="it">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>myip – IP & RIPE Lookup</title>
|
<title>SolidData – IP & RIPE Lookup</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
<link rel="icon" type="image/x-icon" href="/static/favicon.png">
|
||||||
<link rel="stylesheet" href="/static/myip.css">
|
<link rel="stylesheet" href="/static/myip.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
{% if show_logo %}
|
{% if show_logo %}
|
||||||
<a href="https://www.netech-solution.it" class="logo-link" target="_blank" rel="noopener">
|
<a href="https://soliddata.cloud" class="logo-link" target="_blank" rel="noopener">
|
||||||
<img src="/static/logo.png" class="logo" alt="Logo">
|
<img src="/static/logo.png" class="logo" alt="Logo">
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="badge">
|
<div class="badge">
|
||||||
<span class="badge-dot"></span>
|
<span class="badge-dot"></span>
|
||||||
<span>IP & RIPE Lookup</span>
|
<span>IP & RIPE Lookup</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 class="title">Il tuo IP pubblico è:</h1>
|
<h1 class="title">Il tuo IP pubblico è:</h1>
|
||||||
|
|
||||||
<div class="ip-wrapper">
|
<div class="ip-wrapper">
|
||||||
@@ -45,39 +41,20 @@ HTML_TEMPLATE = """<!doctype html>
|
|||||||
<span class="copy-label">Copia IP</span>
|
<span class="copy-label">Copia IP</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
{% if ripe %}
|
{% if ripe %}
|
||||||
<div class="ripe-title">Informazioni RIPE</div>
|
<div class="ripe-title">Informazioni RIPE</div>
|
||||||
|
|
||||||
{% if ripe.holder %}
|
{% if ripe.holder %}
|
||||||
<div class="ripe-item"><strong>Provider:</strong> {{ ripe.holder }}</div>
|
<div class="ripe-item"><strong>Provider:</strong> {{ ripe.holder }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if ripe.prefix %}
|
{% if ripe.prefix %}
|
||||||
<div class="ripe-item"><strong>Prefisso annunciato:</strong> {{ ripe.prefix }}</div>
|
<div class="ripe-item"><strong>Prefisso:</strong> {{ ripe.prefix }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if ripe.asns %}
|
{% if ripe.asns %}
|
||||||
<div class="ripe-item"><strong>ASN:</strong> {{ ripe.asns | join(', ') }}</div>
|
<div class="ripe-item"><strong>ASN:</strong> {{ ripe.asns | join(', ') }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if ripe.routing_status %}
|
|
||||||
<div class="ripe-item"><strong>Routing Status:</strong> {{ ripe.routing_status }}</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if ripe.reverse_dns %}
|
|
||||||
<div class="ripe-item"><strong>Reverse DNS:</strong> {{ ripe.reverse_dns }}</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if ripe.geoloc %}
|
|
||||||
<div class="ripe-item">
|
|
||||||
<strong>GeoLocation:</strong>
|
|
||||||
{% if ripe.geoloc.code %}
|
|
||||||
<img src="https://flagsapi.com/{{ ripe.geoloc.code }}/flat/24.png" class="flag-icon" alt="{{ ripe.geoloc.code }}">
|
|
||||||
{% endif %}
|
|
||||||
{{ ripe.geoloc.country }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if ripe.abuse %}
|
|
||||||
<div class="ripe-item"><strong>Abuse Contact:</strong> {{ ripe.abuse }}</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="ripe-muted">Dati ottenuti tramite RIPEstat Data API.</div>
|
<div class="ripe-muted">Dati ottenuti tramite RIPEstat Data API.</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="ripe-title">Informazioni RIPE</div>
|
<div class="ripe-title">Informazioni RIPE</div>
|
||||||
@@ -100,18 +77,15 @@ HTML_TEMPLATE = """<!doctype html>
|
|||||||
var ip = ipSpan.textContent.trim();
|
var ip = ipSpan.textContent.trim();
|
||||||
if (!ip) return;
|
if (!ip) return;
|
||||||
|
|
||||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
||||||
navigator.clipboard.writeText(ip).then(function () {
|
navigator.clipboard.writeText(ip).then(function () {
|
||||||
var labelEl = btn.querySelector(".copy-label");
|
var original = btn.querySelector(".copy-label").textContent;
|
||||||
var original = labelEl.textContent;
|
|
||||||
btn.classList.add("copy-btn-success");
|
btn.classList.add("copy-btn-success");
|
||||||
labelEl.textContent = "Copiato!";
|
btn.querySelector(".copy-label").textContent = "Copiato!";
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
btn.classList.remove("copy-btn-success");
|
btn.classList.remove("copy-btn-success");
|
||||||
labelEl.textContent = original;
|
btn.querySelector(".copy-label").textContent = original;
|
||||||
}, 1500);
|
}, 1500);
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -119,112 +93,50 @@ HTML_TEMPLATE = """<!doctype html>
|
|||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
RIPESTAT_BASE = "https://stat.ripe.net/data"
|
||||||
|
|
||||||
def get_client_ip():
|
def get_client_ip():
|
||||||
"""Determina l'IP reale del client dietro proxy / load balancer."""
|
|
||||||
xff = request.headers.get("X-Forwarded-For", "")
|
xff = request.headers.get("X-Forwarded-For", "")
|
||||||
if xff:
|
if xff:
|
||||||
parts = [p.strip() for p in xff.split(",") if p.strip()]
|
parts = [p.strip() for p in xff.split(",") if p.strip()]
|
||||||
if parts:
|
if parts:
|
||||||
return parts[0]
|
return parts[0]
|
||||||
|
|
||||||
xri = request.headers.get("X-Real-IP")
|
xri = request.headers.get("X-Real-IP")
|
||||||
if xri:
|
if xri:
|
||||||
return xri.strip()
|
return xri.strip()
|
||||||
|
|
||||||
return request.remote_addr or "Sconosciuto"
|
return request.remote_addr or "Sconosciuto"
|
||||||
|
|
||||||
|
|
||||||
def fetch_ripe_info(ip: str):
|
def fetch_ripe_info(ip: str):
|
||||||
"""Recupera informazioni RIPEstat estese per l'IP."""
|
|
||||||
info = {
|
|
||||||
"prefix": None,
|
|
||||||
"asns": [],
|
|
||||||
"holder": None,
|
|
||||||
"routing_status": None,
|
|
||||||
"reverse_dns": None,
|
|
||||||
"geoloc": None,
|
|
||||||
"abuse": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 1. Network info (prefisso + ASN)
|
ni_resp = requests.get(f"{RIPESTAT_BASE}/network-info/data.json", params={"resource": ip}, timeout=2)
|
||||||
ni = requests.get(
|
ni_resp.raise_for_status()
|
||||||
f"{RIPESTAT_BASE}/network-info/data.json",
|
ni_data = ni_resp.json().get("data", {})
|
||||||
params={"resource": ip},
|
|
||||||
timeout=2,
|
|
||||||
).json().get("data", {})
|
|
||||||
|
|
||||||
info["prefix"] = ni.get("prefix")
|
prefix = ni_data.get("prefix")
|
||||||
asns = ni.get("asns") or []
|
asns = ni_data.get("asns") or []
|
||||||
info["asns"] = [f"AS{a}" for a in asns]
|
holder = None
|
||||||
|
|
||||||
# 2. AS overview (holder/provider)
|
|
||||||
if asns:
|
if asns:
|
||||||
ao = requests.get(
|
first_asn = asns[0]
|
||||||
f"{RIPESTAT_BASE}/as-overview/data.json",
|
ao_resp = requests.get(f"{RIPESTAT_BASE}/as-overview/data.json", params={"resource": first_asn}, timeout=2)
|
||||||
params={"resource": asns[0]},
|
ao_resp.raise_for_status()
|
||||||
timeout=2,
|
holder = ao_resp.json().get("data", {}).get("holder")
|
||||||
).json().get("data", {})
|
|
||||||
info["holder"] = ao.get("holder")
|
|
||||||
|
|
||||||
# 3. Routing status / visibility
|
class R: pass
|
||||||
rs = requests.get(
|
r = R()
|
||||||
f"{RIPESTAT_BASE}/routing-status/data.json",
|
r.prefix = prefix
|
||||||
params={"resource": ip},
|
r.asns = [f"AS{x}" for x in asns] if asns else []
|
||||||
timeout=2,
|
r.holder = holder
|
||||||
).json().get("data", {})
|
|
||||||
info["routing_status"] = rs.get("status")
|
|
||||||
|
|
||||||
# 4. Reverse DNS
|
|
||||||
rd = requests.get(
|
|
||||||
f"{RIPESTAT_BASE}/reverse-dns/data.json",
|
|
||||||
params={"resource": ip},
|
|
||||||
timeout=2,
|
|
||||||
).json().get("data", {}).get("result", [])
|
|
||||||
info["reverse_dns"] = rd[0].get("name") if rd else None
|
|
||||||
|
|
||||||
# 5. GeoLocation
|
|
||||||
gl = requests.get(
|
|
||||||
f"{RIPESTAT_BASE}/geoloc/data.json",
|
|
||||||
params={"resource": ip},
|
|
||||||
timeout=2,
|
|
||||||
).json().get("data", {})
|
|
||||||
country = None
|
|
||||||
if gl.get("locations"):
|
|
||||||
country = gl["locations"][0].get("country", {})
|
|
||||||
if country:
|
|
||||||
info["geoloc"] = {
|
|
||||||
"country": country.get("name"),
|
|
||||||
"code": country.get("code"),
|
|
||||||
}
|
|
||||||
|
|
||||||
# 6. Abuse Contact
|
|
||||||
ac = requests.get(
|
|
||||||
f"{RIPESTAT_BASE}/abuse-contact-finder/data.json",
|
|
||||||
params={"resource": ip},
|
|
||||||
timeout=2,
|
|
||||||
).json().get("data", {})
|
|
||||||
emails = ac.get("emails") or []
|
|
||||||
info["abuse"] = emails[0] if emails else None
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
# In caso di errore lasciamo info parziali / vuote
|
|
||||||
pass
|
|
||||||
|
|
||||||
return info
|
|
||||||
|
|
||||||
|
return r if any([r.prefix, r.asns, r.holder]) else None
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
ip = get_client_ip()
|
ip = get_client_ip()
|
||||||
ripe_info = fetch_ripe_info(ip) if ip != "Sconosciuto" else None
|
ripe = fetch_ripe_info(ip) if ip != "Sconosciuto" else None
|
||||||
return render_template_string(
|
return render_template_string(HTML_TEMPLATE, ip=ip, ripe=ripe, show_logo=True)
|
||||||
HTML_TEMPLATE,
|
|
||||||
ip=ip,
|
|
||||||
ripe=ripe_info,
|
|
||||||
show_logo=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0", port=8000)
|
app.run(host="0.0.0.0", port=8000)
|
||||||
|
|||||||
BIN
static/logo.png
BIN
static/logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 11 KiB |
@@ -19,11 +19,19 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.badge { margin-bottom: 1rem; }
|
||||||
.title {
|
.title {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
color: #101F2D;
|
color: #101F2D;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
.ip-wrapper { margin-top: 1rem; margin-bottom: 1rem; }
|
||||||
|
.ip { font-size: 3em; font-weight: 600; color: #0181C4; }
|
||||||
|
.divider {
|
||||||
|
border-bottom: 1px solid #0181C4;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
.ripe-title {
|
.ripe-title {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -35,30 +43,11 @@ body {
|
|||||||
color: #101F2D;
|
color: #101F2D;
|
||||||
margin: 0.2rem 0;
|
margin: 0.2rem 0;
|
||||||
}
|
}
|
||||||
|
.ripe-muted {
|
||||||
/* Logo link wrapper */
|
font-size: .8rem;
|
||||||
.logo-link {
|
margin-top: 30px;
|
||||||
display: inline-block;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
}
|
||||||
|
/* Logo ridimensionato correttamente */
|
||||||
/* Bandierina geoloc */
|
|
||||||
.flag-icon {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-right: 6px;
|
|
||||||
vertical-align: middle;
|
|
||||||
box-shadow: 0 0 3px rgba(0,0,0,0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Bottone copia IP sotto l'indirizzo */
|
|
||||||
.copy-btn-below {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Logo size constraints */
|
|
||||||
.logo {
|
.logo {
|
||||||
max-width: 260px;
|
max-width: 260px;
|
||||||
max-height: 100px;
|
max-height: 100px;
|
||||||
@@ -68,3 +57,28 @@ body {
|
|||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* bottone sotto l'IP */
|
||||||
|
.copy-btn-below {
|
||||||
|
font-family: 'Montserrat', Helvetica, Arial, sans-serif;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
background: #101F2D;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1.2em;
|
||||||
|
padding: 8px 20px;
|
||||||
|
transition: all .3s;
|
||||||
|
border:0;
|
||||||
|
}
|
||||||
|
.copy-btn-below:hover, .copy-btn-below:active {
|
||||||
|
font-family: 'Montserrat', Helvetica, Arial, sans-serif;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
background: #79858B;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1.2em;
|
||||||
|
padding: 8px 20px;
|
||||||
|
transition: all .3s;
|
||||||
|
border:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user