fix ui
All checks were successful
Deploy on Master Change / deploy (push) Successful in 1m15s

This commit is contained in:
2026-07-13 10:04:56 +07:00
parent 58c96609b6
commit b4c3155564
11 changed files with 587 additions and 97 deletions

View File

@@ -16,6 +16,7 @@ import (
"io"
"log"
"mime/multipart"
"net"
"net/http"
"net/url"
"os"
@@ -154,6 +155,7 @@ type App struct {
wifiRejected bool
quitDialogShown bool
monitoringTornDown bool
localBrowserActive bool // trình duyệt local — chỉ cho phép localhost
chatUnread int
replyStaffID uint
fetchAppsMu sync.Mutex
@@ -276,6 +278,9 @@ func (a *App) startup(ctx context.Context) {
// Khởi chạy vòng lặp đọc local storage của trang Rikkei Portal khi chưa đăng nhập
go a.authStorageScanner()
// Chặn thoát localhost khi đang dùng trình duyệt local
go a.localBrowserGuardLoop()
}
func (a *App) handleGuardViolation(kind, reason string) {
@@ -588,6 +593,7 @@ func (a *App) startLocalServer() {
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
return
}
a.setLocalBrowserActive(false)
go func() {
guard.SuppressFor(5 * time.Second)
runtime.WindowReloadApp(a.ctx)
@@ -604,6 +610,46 @@ func (a *App) startLocalServer() {
go a.purgeWebViewDiskCache()
w.Write([]byte("ok"))
})
mux.HandleFunc("/local-browser", func(w http.ResponseWriter, r *http.Request) {
blocked := strings.TrimSpace(r.URL.Query().Get("blocked"))
from := strings.TrimSpace(r.URL.Query().Get("from"))
w.Header().Set("Content-Type", "text/html; charset=utf-8")
notice := ""
if blocked == "1" {
notice = `<div class="notice">Đã chặn liên kết ngoài mạng. Trình duyệt này chỉ mở <strong>localhost</strong> / <strong>127.0.0.1</strong>.</div>`
if from != "" {
notice += `<div class="notice-sub">Đã chặn: ` + html.EscapeString(from) + `</div>`
}
}
_, _ = fmt.Fprintf(w, localBrowserHTML, notice)
})
mux.HandleFunc("/local-browser-go", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
return
}
raw := strings.TrimSpace(r.URL.Query().Get("url"))
if raw == "" {
http.Error(w, "missing url", http.StatusBadRequest)
return
}
if !strings.Contains(raw, "://") {
raw = "http://" + raw
}
if !isLoopbackURL(raw) {
http.Error(w, "only localhost allowed", http.StatusForbidden)
return
}
a.mu.Lock()
a.localBrowserActive = true
a.mu.Unlock()
go func() {
guard.SuppressFor(3 * time.Second)
runtime.WindowExecJS(a.ctx, fmt.Sprintf("window.location.href = %q", raw))
}()
w.Write([]byte("ok"))
})
go func() {
_ = server.ListenAndServe()
@@ -690,6 +736,94 @@ document.addEventListener('keydown', function(e){
</body>
</html>`
const localBrowserHTML = `<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Trình duyệt Local — Simple Care</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:Segoe UI,system-ui,sans-serif;background:#f5f7fa;color:#1a2332;min-height:100vh;display:flex;flex-direction:column}
.bar{display:flex;align-items:center;gap:8px;padding:10px 14px;background:#fff;border-bottom:1px solid #e4e9f0;flex-wrap:wrap}
.bar button{border-radius:8px;padding:8px 12px;font-size:13px;cursor:pointer;font-weight:600;white-space:nowrap}
.btn-back{background:#fff;color:#bb2126;border:1px solid #bb2126}
.btn-back:hover{background:#fde8e9}
.btn-go{background:#bb2126;color:#fff;border:1px solid #bb2126}
.btn-go:hover{background:#9e1c22}
.addr{flex:1;min-width:220px;padding:8px 12px;border:1px solid #e4e9f0;border-radius:8px;font-size:13px}
.addr:focus{outline:2px solid rgba(187,33,38,.25);border-color:#bb2126}
.main{flex:1;padding:28px 20px;max-width:720px;margin:0 auto;width:100%%}
h1{font-size:1.35rem;margin-bottom:8px;color:#bb2126}
.lead{color:#5a6a7e;font-size:0.95rem;margin-bottom:18px;line-height:1.5}
.notice{background:#fde8e9;color:#9e1c22;border:1px solid #f5c2c4;border-radius:10px;padding:12px 14px;margin-bottom:10px;font-size:0.9rem}
.notice-sub{font-size:0.8rem;color:#8b99a8;margin-bottom:14px;word-break:break-all}
.chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:16px}
.chip{background:#fff;border:1px solid #e4e9f0;border-radius:999px;padding:6px 12px;font-size:12px;cursor:pointer;color:#bb2126;font-weight:600}
.chip:hover{background:#fde8e9;border-color:#bb2126}
.hint{margin-top:22px;font-size:12px;color:#8b99a8;line-height:1.5}
</style>
</head>
<body>
<div class="bar">
<button type="button" class="btn-back" onclick="goHome()">← Về trang chính</button>
<input class="addr" id="addr" type="url" placeholder="http://localhost:3000" spellcheck="false" />
<button type="button" class="btn-go" onclick="goLocal()">Mở</button>
</div>
<div class="main">
%s
<h1>Trình duyệt Local</h1>
<p class="lead">Chỉ dùng để xem bài làm chạy trên máy bạn (<strong>localhost</strong> / <strong>127.0.0.1</strong>). Mọi link online (GitHub, Google, …) đều bị chặn tại đây.</p>
<div class="chips">
<button type="button" class="chip" onclick="fill('http://localhost:3000')">localhost:3000</button>
<button type="button" class="chip" onclick="fill('http://localhost:5173')">localhost:5173</button>
<button type="button" class="chip" onclick="fill('http://127.0.0.1:8080')">127.0.0.1:8080</button>
<button type="button" class="chip" onclick="fill('http://localhost:5500')">localhost:5500</button>
</div>
<p class="hint">Mẹo: chạy dự án (npm run dev / Live Server), dán URL vào ô trên rồi bấm Mở. Về trang chính: nút ← hoặc menu Simple Care → Ctrl+H.</p>
</div>
<script>
function fill(u){ document.getElementById('addr').value = u; }
function goHome(){
fetch('http://127.0.0.1:34115/exam-close').catch(function(){});
}
function isLoopback(raw){
try {
if (!raw) return false;
if (raw.indexOf('://') < 0) raw = 'http://' + raw;
var u = new URL(raw);
if (u.protocol !== 'http:' && u.protocol !== 'https:') return false;
var h = (u.hostname || '').toLowerCase();
if (h === 'localhost' || h === '127.0.0.1' || h === '::1' || h === '[::1]') return true;
if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
return false;
} catch (e) { return false; }
}
async function goLocal(){
var raw = (document.getElementById('addr').value || '').trim();
if (!raw) { alert('Nhập URL localhost'); return; }
if (!isLoopback(raw)) {
alert('Chỉ được mở localhost / 127.0.0.1');
return;
}
if (raw.indexOf('://') < 0) raw = 'http://' + raw;
try {
var res = await fetch('http://127.0.0.1:34115/local-browser-go?url=' + encodeURIComponent(raw));
if (!res.ok) {
alert('Không mở được — chỉ localhost');
return;
}
} catch (e) {
location.href = raw;
}
}
document.getElementById('addr').addEventListener('keydown', function(e){
if (e.key === 'Enter') { e.preventDefault(); goLocal(); }
});
</script>
</body>
</html>`
// loadSession nạp session từ file cục bộ
func (a *App) loadSession() {
a.mu.Lock()
@@ -808,6 +942,7 @@ func (a *App) GetStudentInfo() map[string]any {
// NavigateToLogin chuyển WebView đến trang đăng nhập Rikkei Portal
func (a *App) NavigateToLogin() {
a.setLocalBrowserActive(false)
a.mu.Lock()
a.expectingLogin = true
a.mu.Unlock()
@@ -1844,10 +1979,88 @@ func (a *App) examStudentContext() (int64, *StudentExamSnapshot, error) {
// ReturnToDashboard quay lại giao diện chính của app (giữ cookie WebView2).
func (a *App) ReturnToDashboard() {
a.setLocalBrowserActive(false)
guard.SuppressFor(5 * time.Second)
runtime.WindowReloadApp(a.ctx)
}
func (a *App) setLocalBrowserActive(on bool) {
a.mu.Lock()
a.localBrowserActive = on
a.mu.Unlock()
}
func isLoopbackURL(raw string) bool {
raw = strings.TrimSpace(raw)
if raw == "" {
return false
}
if !strings.Contains(raw, "://") {
raw = "http://" + raw
}
u, err := url.Parse(raw)
if err != nil || u.Host == "" {
return false
}
scheme := strings.ToLower(u.Scheme)
if scheme != "http" && scheme != "https" {
return false
}
host := strings.ToLower(u.Hostname())
if host == "localhost" {
return true
}
ip := net.ParseIP(host)
return ip != nil && ip.IsLoopback()
}
func (a *App) localBrowserGuardLoop() {
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
for range ticker.C {
a.mu.Lock()
active := a.localBrowserActive && a.ctx != nil && !a.monitoringTornDown
ctx := a.ctx
a.mu.Unlock()
if !active {
continue
}
// Nếu hostname không phải loopback → đá về trang local-browser
runtime.WindowExecJS(ctx, `
(function(){
try {
var h = (location.hostname || '').toLowerCase();
var ok = h === 'localhost' || h === '127.0.0.1' || h === '::1' || h === '[::1]' || /^127\.\d+\.\d+\.\d+$/.test(h);
if (!ok) {
location.href = 'http://127.0.0.1:34115/local-browser?blocked=1&from=' + encodeURIComponent(location.href);
}
} catch (e) {}
})();
`)
}
}
// OpenGitHub mở github.com trong WebView (về trang chính: Ctrl+H).
func (a *App) OpenGitHub() {
a.setLocalBrowserActive(false)
guard.SuppressFor(5 * time.Second)
runtime.WindowExecJS(a.ctx, `window.location.href = 'https://github.com'`)
}
// OpenGoogleTranslate mở Google Dịch trong WebView.
func (a *App) OpenGoogleTranslate() {
a.setLocalBrowserActive(false)
guard.SuppressFor(5 * time.Second)
runtime.WindowExecJS(a.ctx, `window.location.href = 'https://translate.google.com/?sl=auto&tl=vi'`)
}
// OpenLocalBrowser mở trình duyệt chỉ cho phép localhost (test bài làm local).
func (a *App) OpenLocalBrowser() {
a.setLocalBrowserActive(true)
guard.SuppressFor(5 * time.Second)
runtime.WindowExecJS(a.ctx, `window.location.href = 'http://127.0.0.1:34115/local-browser'`)
}
// ReloadExamPage — F5: làm mới trang / iframe đang mở (trắc nghiệm).
func (a *App) ReloadExamPage() {
guard.SuppressFor(3 * time.Second)
@@ -1928,6 +2141,7 @@ func (a *App) openExamWebView(targetURL, title string) error {
if targetURL == "" {
return errors.New("không có nội dung để mở")
}
a.setLocalBrowserActive(false)
// URL local (PDF/resource): khung exam-view. URL ngoài (quiz): mở top-level giữ first-party cookies/session.
if isLocalExamURL(targetURL) {
wrapper := fmt.Sprintf(