Fix demo login credentials and form encoding
CI / basic-check (push) Has been cancelled

This commit is contained in:
Perplexity Bot
2026-08-04 20:25:33 +00:00
parent 8aae681706
commit a6d0dd6c68
3 changed files with 95 additions and 22 deletions
+45 -9
View File
@@ -5,39 +5,75 @@ from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt from jose import JWTError, jwt
from passlib.context import CryptContext from passlib.context import CryptContext
from pydantic import BaseModel from pydantic import BaseModel
SECRET_KEY = 'change-me-in-production' SECRET_KEY = 'change-me-in-production'
ALGORITHM = 'HS256' ALGORITHM = 'HS256'
ACCESS_TOKEN_EXPIRE_MINUTES = 120 ACCESS_TOKEN_EXPIRE_MINUTES = 120
pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto') pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')
oauth2_scheme = OAuth2PasswordBearer(tokenUrl='/auth/token') oauth2_scheme = OAuth2PasswordBearer(tokenUrl='/auth/token')
class Token(BaseModel): access_token: str; token_type: str = 'bearer'
class TokenData(BaseModel): username: Optional[str] = None class Token(BaseModel):
class User(BaseModel): username: str; disabled: bool = False; role: str = 'viewer' access_token: str
class UserInDB(User): hashed_password: str token_type: str = 'bearer'
fake_users_db = {'admin': {'username': 'admin', 'hashed_password': pwd_context.hash('admin123'), 'disabled': False, 'role': 'admin'}, 'viewer': {'username': 'viewer', 'hashed_password': pwd_context.hash('viewer123'), 'disabled': False, 'role': 'viewer'}}
def verify_password(plain_password: str, hashed_password: str) -> bool: return pwd_context.verify(plain_password, hashed_password) class TokenData(BaseModel):
username: Optional[str] = None
class User(BaseModel):
username: str
disabled: bool = False
role: str = 'viewer'
class UserInDB(User):
hashed_password: str
fake_users_db = {
'admin': {
'username': 'admin',
'hashed_password': '$2b$12$j2rrsvzYhC9lQ4w6WJ1wPeY8CKEMMvmFo0xSg6u40qCMgfHdCqkfG',
'disabled': False,
'role': 'admin',
},
'viewer': {
'username': 'viewer',
'hashed_password': '$2b$12$DA7Nn4MVSr1m3Q0P6x1Qe.i6yd0qJ7Yx1C2VYLRNvKcJsteVEh9W6',
'disabled': False,
'role': 'viewer',
},
}
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_user(username: str) -> Optional[UserInDB]: def get_user(username: str) -> Optional[UserInDB]:
user = fake_users_db.get(username) user = fake_users_db.get(username)
return UserInDB(**user) if user else None return UserInDB(**user) if user else None
def authenticate_user(username: str, password: str) -> Optional[UserInDB]: def authenticate_user(username: str, password: str) -> Optional[UserInDB]:
user = get_user(username) user = get_user(username)
return user if user and verify_password(password, user.hashed_password) else None return user if user and verify_password(password, user.hashed_password) else None
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
to_encode = data.copy() to_encode = data.copy()
expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)) expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
to_encode.update({'exp': expire}) to_encode.update({'exp': expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
def get_current_user(token: str = Depends(oauth2_scheme)) -> User: def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
credentials_exception = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Could not validate credentials', headers={'WWW-Authenticate': 'Bearer'}) credentials_exception = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Could not validate credentials', headers={'WWW-Authenticate': 'Bearer'})
try: try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get('sub') username: str = payload.get('sub')
if username is None: raise credentials_exception if username is None:
raise credentials_exception
except JWTError: except JWTError:
raise credentials_exception raise credentials_exception
user = get_user(username) user = get_user(username)
if user is None: raise credentials_exception if user is None:
raise credentials_exception
return User(username=user.username, disabled=user.disabled, role=user.role) return User(username=user.username, disabled=user.disabled, role=user.role)
def get_current_active_user(current_user: User = Depends(get_current_user)) -> User: def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
if current_user.disabled: raise HTTPException(status_code=400, detail='Inactive user') if current_user.disabled:
raise HTTPException(status_code=400, detail='Inactive user')
return current_user return current_user
+3 -6
View File
@@ -24,16 +24,13 @@ body { margin: 0; background: linear-gradient(180deg, #0b1020 0%, #111827 100%);
.shell-card.warning { border-color: rgba(251, 191, 36, 0.35); } .shell-card.warning { border-color: rgba(251, 191, 36, 0.35); }
.shell-card-body { margin-top: 12px; } .shell-card-body { margin-top: 12px; }
.shell-list { margin: 0; padding-left: 18px; display: grid; gap: 8px; color: #cbd5e1; } .shell-list { margin: 0; padding-left: 18px; display: grid; gap: 8px; color: #cbd5e1; }
.options-roadmap { display: grid; gap: 12px; }
.options-roadmap-item { display: grid; grid-template-columns: 40px minmax(0, 1fr); gap: 12px; align-items: start; padding: 12px; border-radius: 12px; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); }
.options-roadmap-item p { margin: 6px 0 0; color: #94a3b8; }
.options-step { width: 32px; height: 32px; border-radius: 999px; display: grid; place-items: center; background: rgba(37, 99, 235, 0.18); color: #bfdbfe; font-weight: 700; }
.login-page { min-height: 100vh; display: grid; place-items: center; padding: 24px; } .login-page { min-height: 100vh; display: grid; place-items: center; padding: 24px; }
.login-card { width: min(100%, 420px); background: rgba(17,24,39,0.92); border: 1px solid rgba(255,255,255,0.08); border-radius: 20px; padding: 24px; } .login-card { width: min(100%, 420px); background: rgba(17,24,39,0.92); border: 1px solid rgba(255,255,255,0.08); border-radius: 20px; padding: 24px; }
.login-eyebrow { color: #93c5fd; text-transform: uppercase; letter-spacing: 0.12em; font-size: 12px; } .login-eyebrow { color: #93c5fd; text-transform: uppercase; letter-spacing: 0.12em; font-size: 12px; }
.login-copy, .login-error { color: #94a3b8; } .login-copy, .login-hint { color: #94a3b8; }
.login-error { color: #fca5a5; }
.login-form { display: grid; gap: 12px; margin-top: 16px; } .login-form { display: grid; gap: 12px; margin-top: 16px; }
.login-form label { display: grid; gap: 6px; } .login-form label { display: grid; gap: 6px; }
.login-form input { border: 1px solid rgba(255,255,255,0.1); background: rgba(255,255,255,0.03); border-radius: 12px; padding: 12px 14px; } .login-form input { border: 1px solid rgba(255,255,255,0.1); background: rgba(255,255,255,0.03); border-radius: 12px; padding: 12px 14px; color: #e5e7eb; }
.login-form button { border: 0; border-radius: 12px; padding: 12px 14px; background: #2563eb; color: white; font-weight: 600; } .login-form button { border: 0; border-radius: 12px; padding: 12px 14px; background: #2563eb; color: white; font-weight: 600; }
@media (max-width: 1100px) { .app-shell { grid-template-columns: 1fr; } .app-sidebar { border-right: 0; border-bottom: 1px solid rgba(255,255,255,0.08); } .shell-grid, .shell-grid.two-columns { grid-template-columns: 1fr; } } @media (max-width: 1100px) { .app-shell { grid-template-columns: 1fr; } .app-sidebar { border-right: 0; border-bottom: 1px solid rgba(255,255,255,0.08); } .shell-grid, .shell-grid.two-columns { grid-template-columns: 1fr; } }
+48 -8
View File
@@ -1,19 +1,59 @@
import { useState } from 'react'; import { useState } from 'react';
type Props = { onLogin: (token: string) => void }; type Props = { onLogin: (token: string) => void };
export function LoginPage({ onLogin }: Props) { export function LoginPage({ onLogin }: Props) {
const [username, setUsername] = useState('admin'); const [username, setUsername] = useState('admin');
const [password, setPassword] = useState('admin123'); const [password, setPassword] = useState('admin123');
const [error, setError] = useState(''); const [error, setError] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const submit = async (e: React.FormEvent) => { const submit = async (e: React.FormEvent) => {
e.preventDefault(); setLoading(true); setError(''); e.preventDefault();
setLoading(true);
setError('');
try { try {
const body = new URLSearchParams(); body.set('username', username); body.set('password', password); const body = new URLSearchParams();
const res = await fetch('/auth/token', { method: 'POST', body }); body.set('username', username);
if (!res.ok) throw new Error('Login fallito'); body.set('password', password);
const data = await res.json(); onLogin(data.access_token); const res = await fetch('/auth/token', {
} catch (err) { setError(err instanceof Error ? err.message : 'Login fallito'); } finally { setLoading(false); } method: 'POST',
}; headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
return <main className="login-page"><section className="login-card"><p className="login-eyebrow">NetMapper auth</p><h1>Accedi al book of truth</h1><p className="login-copy">L'accesso è protetto per impedire lettura e modifica dei dati da terzi.</p><form onSubmit={submit} className="login-form"><label>Username<input value={username} onChange={(e) => setUsername(e.target.value)} /></label><label>Password<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} /></label>{error && <p className="login-error">{error}</p>}<button type="submit" disabled={loading}>{loading ? 'Accesso...' : 'Entra'}</button></form></section></main>; body: body.toString(),
});
if (!res.ok) {
const data = await res.json().catch(() => null);
throw new Error(data?.detail ?? 'Login fallito');
}
const data = await res.json();
onLogin(data.access_token);
} catch (err) {
setError(err instanceof Error ? err.message : 'Login fallito');
} finally {
setLoading(false);
}
};
return (
<main className="login-page">
<section className="login-card">
<p className="login-eyebrow">NetMapper auth</p>
<h1>Accedi al book of truth</h1>
<p className="login-copy">L'accesso è protetto per impedire lettura e modifica dei dati da terzi.</p>
<form onSubmit={submit} className="login-form">
<label>
Username
<input value={username} onChange={(e) => setUsername(e.target.value)} autoComplete="username" />
</label>
<label>
Password
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="current-password" />
</label>
<p className="login-hint">Credenziali demo: admin / admin123 oppure viewer / viewer123</p>
{error && <p className="login-error">{error}</p>}
<button type="submit" disabled={loading}>{loading ? 'Accesso...' : 'Entra'}</button>
</form>
</section>
</main>
);
} }