feat: MFA prompt in user settings page
This commit is contained in:
parent
48cda41d69
commit
44326c542d
3 changed files with 84 additions and 1 deletions
|
|
@ -1,5 +1,11 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.1.0.0044 — 2026-07-11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- MFA/TOTP check on user settings page — shows setup prompt if no authenticator is configured
|
||||||
|
- `/api/user/mfa-status` endpoint — checks Authelia for TOTP enrollment status
|
||||||
|
|
||||||
## 0.1.0.0043 — 2026-07-11
|
## 0.1.0.0043 — 2026-07-11
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.1.0.0043
|
0.1.0.0044
|
||||||
|
|
|
||||||
77
main.go
77
main.go
|
|
@ -753,6 +753,37 @@ func publicSettingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
settings.Company.Name, settings.Company.Subtitle, settings.Company.Logo)
|
settings.Company.Name, settings.Company.Subtitle, settings.Company.Logo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- MFA status check ---
|
||||||
|
|
||||||
|
func checkMFAStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user := r.Header.Get("Remote-User")
|
||||||
|
if user == "" {
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call Authelia API to check TOTP status
|
||||||
|
token := os.Getenv("AUTHELIA_SECRET")
|
||||||
|
req, _ := http.NewRequest("GET", "http://authelia:9091/api/user/info", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
json.NewEncoder(w).Encode(map[string]bool{"mfa_enabled": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var userInfo struct {
|
||||||
|
TOTP bool `json:"totp"`
|
||||||
|
}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&userInfo)
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]bool{
|
||||||
|
"mfa_enabled": userInfo.TOTP,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// --- Translation system ---
|
// --- Translation system ---
|
||||||
|
|
||||||
var translations = make(map[string]map[string]string)
|
var translations = make(map[string]map[string]string)
|
||||||
|
|
@ -954,6 +985,8 @@ const settingsHTML = `<!DOCTYPE html>
|
||||||
.btn { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.88rem; font-weight: 500; cursor: pointer; border: none; }
|
.btn { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.88rem; font-weight: 500; cursor: pointer; border: none; }
|
||||||
.btn-primary { background: #1a1a2e; color: #fff; }
|
.btn-primary { background: #1a1a2e; color: #fff; }
|
||||||
.btn-primary:hover { background: #2d3748; }
|
.btn-primary:hover { background: #2d3748; }
|
||||||
|
.btn-secondary { display: inline-block; background: #1a1a2e; color: #fff; padding: 8px 20px; border-radius: 6px; text-decoration: none; margin-top: 0.5rem; }
|
||||||
|
.btn-secondary:hover { background: #2d3748; }
|
||||||
.actions { display: flex; gap: 0.75rem; align-items: center; margin-top: 1rem; }
|
.actions { display: flex; gap: 0.75rem; align-items: center; margin-top: 1rem; }
|
||||||
.saved-msg { color: #48bb78; font-size: 0.9rem; display: none; }
|
.saved-msg { color: #48bb78; font-size: 0.9rem; display: none; }
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1021,6 +1054,22 @@ const settingsHTML = `<!DOCTYPE html>
|
||||||
<span id="savemsg" class="saved-msg">{{t .Lang "saved"}}</span>
|
<span id="savemsg" class="saved-msg">{{t .Lang "saved"}}</span>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<!-- MFA Section -->
|
||||||
|
<div id="mfa-section" style="display:none;margin-top:2rem;padding:1.5rem;background:#f7fafc;border-radius:8px;border:1px solid #e2e8f0;">
|
||||||
|
<h3>🔐 Two-Factor Authentication</h3>
|
||||||
|
<div id="mfa-active" style="display:none;">
|
||||||
|
<p style="color:#38a169;font-weight:500;">✅ Two-factor authentication is active. Your account is secure.</p>
|
||||||
|
</div>
|
||||||
|
<div id="mfa-setup-prompt">
|
||||||
|
<p>You have configured a work email. For security, enable two-factor authentication with an authenticator app (Google Authenticator, Authy, etc.).</p>
|
||||||
|
<a class="btn-secondary" href="https://auth.nextwks.eu" target="_blank">Set Up Two-Factor Now →</a>
|
||||||
|
<p class="field-note" style="margin-top:0.5rem;">
|
||||||
|
After setting up, click refresh to verify.
|
||||||
|
<button class="btn-small" onclick="checkMFA()" style="padding:0.25rem 0.75rem;border:1px solid #e2e8f0;border-radius:4px;background:#fff;cursor:pointer;">Verify Setup</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{if .IsAdmin}}
|
{{if .IsAdmin}}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Administration</h2>
|
<h2>Administration</h2>
|
||||||
|
|
@ -1030,6 +1079,32 @@ const settingsHTML = `<!DOCTYPE html>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
checkMFA();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function checkMFA() {
|
||||||
|
const resp = await fetch('/api/user/mfa-status');
|
||||||
|
const data = await resp.json();
|
||||||
|
const section = document.getElementById('mfa-section');
|
||||||
|
const prompt = document.getElementById('mfa-setup-prompt');
|
||||||
|
const active = document.getElementById('mfa-active');
|
||||||
|
const emailField = document.querySelector('input[name="email"]');
|
||||||
|
|
||||||
|
if (!emailField || !emailField.value) {
|
||||||
|
section.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
section.style.display = 'block';
|
||||||
|
if (data.mfa_enabled) {
|
||||||
|
prompt.style.display = 'none';
|
||||||
|
active.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
prompt.style.display = 'block';
|
||||||
|
active.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveSettings(e) {
|
async function saveSettings(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const form = document.getElementById('settings-form');
|
const form = document.getElementById('settings-form');
|
||||||
|
|
@ -1037,6 +1112,7 @@ const settingsHTML = `<!DOCTYPE html>
|
||||||
const resp = await fetch('/settings/save', {method:'POST', body:new URLSearchParams(data)});
|
const resp = await fetch('/settings/save', {method:'POST', body:new URLSearchParams(data)});
|
||||||
const msg = document.getElementById('savemsg');
|
const msg = document.getElementById('savemsg');
|
||||||
if (resp.ok) { msg.style.display = 'inline'; setTimeout(() => msg.style.display = 'none', 3000); }
|
if (resp.ok) { msg.style.display = 'inline'; setTimeout(() => msg.style.display = 'none', 3000); }
|
||||||
|
setTimeout(checkMFA, 1000);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -1705,6 +1781,7 @@ func main() {
|
||||||
// Public
|
// Public
|
||||||
mux.HandleFunc("/health", healthHandler)
|
mux.HandleFunc("/health", healthHandler)
|
||||||
mux.HandleFunc("/api/settings/public", publicSettingsHandler)
|
mux.HandleFunc("/api/settings/public", publicSettingsHandler)
|
||||||
|
mux.HandleFunc("/api/user/mfa-status", authMiddleware(checkMFAStatus))
|
||||||
|
|
||||||
// Protectected: launcher
|
// Protectected: launcher
|
||||||
if companyName != "" {
|
if companyName != "" {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue