Вход в панель фермы
Панель открывает администрирование только для роли администратора. Обычный пользователь получит страницу установки VPN.
Форма серверная HTML5: без клиентской авторизации и без тяжелой логики в браузере.
package auth import ( "encoding/json" "fmt" "html/template" "net/http" "os" "path/filepath" "strings" "github.com/example/remote-access-platform/backend/internal/platform/authority" "github.com/example/remote-access-platform/backend/internal/platform/httpx" ) type loginHTMLPage struct { Email string Error string } type vpnDownloadHTMLPage struct { ActorUserID string AndroidAPK string AndroidAPI string BuildInfo string Version string VersionCode string BuildType string Size string SHA256 string PublishedAt string } func (m *Module) handleLoginHTMLPage(w http.ResponseWriter, r *http.Request) { m.renderLoginHTML(w, loginHTMLPage{ Email: strings.TrimSpace(r.URL.Query().Get("email")), Error: strings.TrimSpace(r.URL.Query().Get("error")), }) } func (m *Module) handleLoginHTML(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { m.renderLoginHTML(w, loginHTMLPage{Error: "Не удалось прочитать форму входа."}) return } email := strings.TrimSpace(r.FormValue("email")) result, err := m.service.Login(r.Context(), LoginCommand{ Email: email, Password: r.FormValue("password"), DeviceFingerprint: "web-control-html5", DeviceLabel: "HTML5 control panel", TrustDevice: true, }) if err != nil { _, message := m.service.MapError(err) m.renderLoginHTML(w, loginHTMLPage{Email: email, Error: message}) return } target := "/api/v1/auth/ui/vpn-download?actor_user_id=" + template.URLQueryEscaper(result.User.ID) switch strings.TrimSpace(result.User.PlatformRole) { case authority.PlatformRoleAdmin, authority.PlatformRoleRecoveryAdmin: target = "/api/v1/ui/admin?actor_user_id=" + template.URLQueryEscaper(result.User.ID) } http.Redirect(w, r, target, http.StatusSeeOther) } func (m *Module) handleVPNDownloadHTML(w http.ResponseWriter, r *http.Request) { page := vpnDownloadHTMLPage{ ActorUserID: strings.TrimSpace(r.URL.Query().Get("actor_user_id")), AndroidAPK: "/downloads/rap-android-vpn-latest-debug.apk", AndroidAPI: "/api/v1/downloads/rap-android-vpn-latest-debug.apk", BuildInfo: "/downloads/rap-android-vpn-build.json", } page.loadAndroidBuildInfo() page.applyAndroidDownloadPaths() w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := vpnDownloadHTMLTemplate.Execute(w, page); err != nil { httpx.WriteError(w, http.StatusInternalServerError, "render vpn download page") } } func (m *Module) renderLoginHTML(w http.ResponseWriter, page loginHTMLPage) { w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := loginHTMLTemplate.Execute(w, page); err != nil { httpx.WriteError(w, http.StatusInternalServerError, "render login page") } } func (p *vpnDownloadHTMLPage) loadAndroidBuildInfo() { var manifest struct { Version struct { Name string `json:"name"` Code string `json:"code"` } `json:"version"` Build struct { Type string `json:"type"` } `json:"build"` Published struct { SHA256 string `json:"sha256"` SizeBytes int64 `json:"size_bytes"` TimestampUTC string `json:"timestamp_utc"` } `json:"published"` } releaseDir := strings.TrimSpace(os.Getenv("RAP_RELEASE_DIR")) if releaseDir == "" { releaseDir = "/tmp/rap-release" } data, err := os.ReadFile(filepath.Join(releaseDir, "rap-android-vpn-build.json")) if err != nil || json.Unmarshal(data, &manifest) != nil { return } p.Version = strings.TrimSpace(manifest.Version.Name) p.VersionCode = strings.TrimSpace(manifest.Version.Code) p.BuildType = strings.TrimSpace(manifest.Build.Type) p.SHA256 = strings.TrimSpace(manifest.Published.SHA256) p.PublishedAt = strings.TrimSpace(manifest.Published.TimestampUTC) if manifest.Published.SizeBytes > 0 { p.Size = fmt.Sprintf("%.1f MB", float64(manifest.Published.SizeBytes)/1024/1024) } } func (p *vpnDownloadHTMLPage) applyAndroidDownloadPaths() { version := strings.TrimSpace(p.Version) buildType := strings.TrimSpace(p.BuildType) if version == "" || buildType == "" { return } fileName := "rap-android-vpn-" + version + "-" + buildType + ".apk" p.AndroidAPK = "/downloads/releases/" + version + "/" + fileName p.AndroidAPI = "/api/v1/downloads/releases/" + version + "/" + fileName } var loginHTMLTemplate = template.Must(template.New("login-html").Parse(`
Панель открывает администрирование только для роли администратора. Обычный пользователь получит страницу установки VPN.
Форма серверная HTML5: без клиентской авторизации и без тяжелой логики в браузере.
Для вашей роли доступен установщик Android VPN. Узел Android после установки работает с фермой по протоколу фермы.