package handlers import ( "errors" "fmt" "net/http" "strings" "amnezia-share/internal/i18n" "amnezia-share/internal/member" "amnezia-share/internal/models" "amnezia-share/internal/share" "amnezia-share/internal/web" ) func registerCabinet(a *web.App, mux *http.ServeMux) { mux.HandleFunc("GET /cabinet", a.RequireMember(CabinetGet(a))) mux.HandleFunc("POST /cabinet", a.RequireMember(a.CSRFProtect(CabinetPost(a)))) mux.HandleFunc("GET /cabinet/login", CabinetLoginForm(a)) mux.HandleFunc("POST /cabinet/login", a.CSRFProtect(CabinetLoginSubmit(a))) mux.HandleFunc("GET /cabinet/register", CabinetRegisterForm(a)) mux.HandleFunc("POST /cabinet/register", a.CSRFProtect(CabinetRegisterSubmit(a))) mux.HandleFunc("GET /cabinet/logout", CabinetLogout(a)) mux.HandleFunc("GET /cabinet/servers", CabinetServers(a)) mux.HandleFunc("GET /cabinet/download", CabinetDownload(a)) } type cabinetView struct { Member *models.Member Sub *models.MemberSubscription SubLabel string ConfigsRemaining int Servers []models.ServerInfo Bundles []share.Bundle Maintenance bool Expired bool LimitReached bool OK string Error string } func renderCabinet(a *web.App, w http.ResponseWriter, r *http.Request, okMsg, errText string) { ctx := r.Context() m := a.CurrentMember(r) sub, err := a.Member.EnsureSubscription(ctx, a.Settings, m.ID) if err != nil { errText = err.Error() } bundles, _ := a.Member.Bundles(ctx, m.ID) servers := share.BuildGuestServers(ctx, a.Settings, a.Settings.ServerLabels(ctx)) view := cabinetView{ Member: m, Sub: sub, Servers: servers, Bundles: bundles, Maintenance: a.Settings.Maintenance(ctx), OK: okMsg, Error: errText, } if sub != nil { view.Expired = member.SubscriptionExpired(*sub) view.LimitReached = sub.ConfigCount >= sub.MaxConfigs view.SubLabel = member.SubscriptionExpiresLabel(*sub) view.ConfigsRemaining = sub.MaxConfigs - sub.ConfigCount } a.Render(w, r, "page_cabinet", view) } // CabinetGet renders the member portal dashboard (cabinet.php GET). func CabinetGet(a *web.App) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { renderCabinet(a, w, r, "", "") } } // CabinetPost handles member actions: create / migrate / renew / delete // (cabinet.php POST). func CabinetPost(a *web.App) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() m := a.CurrentMember(r) lang := i18n.Resolve(r, a.Sessions) maintenance := a.Settings.Maintenance(ctx) var actionErr error var okMsg string switch r.FormValue("action") { case "create": if maintenance { actionErr = errors.New(i18n.T(lang, "err_maintenance_mode", nil)) break } serverID := formInt(r, "server_id", -1) protocol := strings.TrimSpace(r.FormValue("protocol")) _, err := a.Member.TryAddConfig(ctx, a.Panel, a.Settings, m.ID, serverID, protocol) if err != nil { actionErr = err break } okMsg = "Готово! Конфигурация создана." case "migrate": if maintenance { actionErr = errors.New(i18n.T(lang, "err_maintenance_mode", nil)) break } configID := formInt64(r, "config_id", 0) newServerID := formInt(r, "new_server_id", -1) newProtocol := strings.TrimSpace(r.FormValue("new_protocol")) if err := a.Member.TryMigrate(ctx, a.Panel, a.Settings, m.ID, configID, newServerID, newProtocol); err != nil { actionErr = err break } okMsg = i18n.T(lang, "ok_migrated", nil) case "renew": code := r.FormValue("code") res, err := a.Member.TryRedeemRenewal(ctx, m.ID, code) if err != nil { actionErr = err break } until := "" if res.Sub != nil { until = member.SubscriptionExpiresLabel(*res.Sub) } okMsg = i18n.T(lang, "ok_renewed", map[string]string{"days": fmt.Sprint(res.AddDays), "until": until}) case "delete": configID := formInt64(r, "config_id", 0) if err := a.Member.SoftDelete(ctx, a.Panel, a.Settings, m.ID, configID); err != nil { actionErr = err break } okMsg = "Конфигурация удалена." default: actionErr = errors.New("Неизвестное действие.") } errText := "" if actionErr != nil { errText = i18n.TranslateGuestError(lang, actionErr.Error()) } if wantsJSON(r) { if errText != "" { writeJSONError(w, http.StatusBadRequest, errText) return } writeJSONOK(w, map[string]any{"message": okMsg}) return } renderCabinet(a, w, r, okMsg, errText) } } type cabinetAuthView struct { Error string } // CabinetLoginForm renders the member login form. func CabinetLoginForm(a *web.App) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if a.CurrentMember(r) != nil { redirectTo(w, r, a, "cabinet") return } a.Render(w, r, "page_cabinet_auth", cabinetAuthView{}) } } // CabinetLoginSubmit verifies member credentials and signs them in. func CabinetLoginSubmit(a *web.App) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() username := strings.TrimSpace(r.FormValue("username")) password := r.FormValue("password") m, err := a.Member.Login(ctx, username, password) if err != nil { a.Render(w, r, "page_cabinet_auth", cabinetAuthView{Error: err.Error()}) return } _ = a.Sessions.RenewToken(ctx) a.Sessions.Put(ctx, web.SessionMemberID, m.ID) redirectTo(w, r, a, "cabinet") } } // CabinetRegisterForm renders the member self-registration form. func CabinetRegisterForm(a *web.App) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if a.CurrentMember(r) != nil { redirectTo(w, r, a, "cabinet") return } a.Render(w, r, "page_cabinet_auth", cabinetAuthView{}) } } // CabinetRegisterSubmit creates a new member account and signs them in. func CabinetRegisterSubmit(a *web.App) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() username := strings.TrimSpace(r.FormValue("username")) password := r.FormValue("password") email := strings.TrimSpace(r.FormValue("email")) m, err := a.Member.Register(ctx, username, password, email) if err != nil { a.Render(w, r, "page_cabinet_auth", cabinetAuthView{Error: err.Error()}) return } if _, err := a.Member.EnsureSubscription(ctx, a.Settings, m.ID); err != nil { a.Render(w, r, "page_cabinet_auth", cabinetAuthView{Error: err.Error()}) return } _ = a.Sessions.RenewToken(ctx) a.Sessions.Put(ctx, web.SessionMemberID, m.ID) redirectTo(w, r, a, "cabinet") } } // CabinetLogout clears the member session. func CabinetLogout(a *web.App) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { a.Sessions.Remove(r.Context(), web.SessionMemberID) _ = a.Sessions.RenewToken(r.Context()) redirectTo(w, r, a, "cabinet/login") } } // CabinetServers returns the server catalog as JSON for the member portal. func CabinetServers(a *web.App) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() servers := share.BuildGuestServers(ctx, a.Settings, a.Settings.ServerLabels(ctx)) writeJSONOK(w, map[string]any{"servers": servers}) } } // CabinetDownload streams a single downloadable artifact for one of the // member's configs. func CabinetDownload(a *web.App) http.HandlerFunc { return a.RequireMember(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() m := a.CurrentMember(r) configID := int64(queryInt(r, "cre", 0)) part := r.URL.Query().Get("part") fp, err := a.Member.DownloadPayload(ctx, m.ID, configID, part) if err != nil || fp == nil { http.Error(w, "Файл не найден.", http.StatusNotFound) return } serveFilePart(w, fp) }) }