diff --git a/frontend/admin/reverse/output/index-CO3BwsT2.pretty.js b/frontend/admin/reverse/output/index-CO3BwsT2.pretty.js index b1b03d2..213ea08 100644 --- a/frontend/admin/reverse/output/index-CO3BwsT2.pretty.js +++ b/frontend/admin/reverse/output/index-CO3BwsT2.pretty.js @@ -292937,7 +292937,11 @@ function c5t() { }, [v, l], ), - E = () => { + E = (e) => { + if (e) { + o(!0); + return; + } (o(!1), a(null), v.reset(i)); }; return Q.jsxs(Jet, { @@ -294416,7 +294420,8 @@ function T5t({ node: e, refetch: t, t: n }) { children: [ Q.jsx(Rot, { className: "cursor-pointer", - onClick: () => { + onSelect: (t) => { + t.preventDefault(); (o(e.type), r(e), i(!0)); }, children: Q.jsxs("div", { @@ -305860,7 +305865,7 @@ function codexNativeIPv6Table() { [r, o] = H.useState({}), [s, a] = H.useState({}), [l, c] = H.useState({ pageIndex: 0, pageSize: 20 }), - [d, u] = H.useState("0"), + [d, u] = H.useState({ open: !1, user: null, planId: "0" }), [h, g] = H.useState(!1), { refetch: p, @@ -305873,16 +305878,30 @@ function codexNativeIPv6Table() { params: { page: l.pageIndex + 1, per_page: l.pageSize, keyword: n }, }), }), - { data: _, refetch: v } = gC({ + { data: _ } = gC({ queryKey: ["codexNativeIPv6Config"], queryFn: () => TL(`${RL()}/user-add-ipv6-subscription/config`), }), - { data: b } = gC({ + { data: v } = gC({ queryKey: ["codexNativeIPv6Plans"], queryFn: () => DD(), }), - y = H.useMemo(() => (Array.isArray(b?.data) ? b.data : []), [b]), - x = H.useMemo( + b = H.useMemo(() => (Array.isArray(v?.data) ? v.data : []), [v]), + y = H.useMemo(() => { + const e = _?.data?.ipv6_plan_id; + return e && e > 0 ? String(e) : "0"; + }, [_]), + x = H.useCallback( + (e) => { + const t = e?.plan_id && e.plan_id > 0 ? String(e.plan_id) : y; + u({ open: !0, user: e, planId: t }); + }, + [y], + ), + w = H.useCallback(() => { + u({ open: !1, user: null, planId: y }); + }, [y]), + C = H.useMemo( () => [ JKt.display({ id: "relation", @@ -305896,16 +305915,10 @@ function codexNativeIPv6Table() { cell: ({ row: e }) => Q.jsx("div", { className: "max-w-[240px] truncate", children: e.getValue("email") || "-" }), }, - { - accessorKey: "ipv6_email", - header: () => "IPv6 account", - cell: ({ row: e }) => - Q.jsx("div", { className: "font-mono text-xs", children: e.getValue("ipv6_email") || "-" }), - }, { accessorKey: "plan_name", header: () => "IPv6 plan", - cell: ({ row: e }) => e.getValue("plan_name") || "-", + cell: ({ row: e }) => e.getValue("plan_name") || "", }, JKt.display({ id: "status", @@ -305915,40 +305928,53 @@ function codexNativeIPv6Table() { JKt.display({ id: "actions", header: () => "Actions", - cell: ({ row: e }) => - Q.jsxs("div", { + cell: ({ row: e }) => { + const t = e.original.is_active || "active" === e.original.status, + n = "disabled" === e.original.status, + i = t ? "Set plan" : n ? "Re-enable" : "Enable"; + return Q.jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [ Q.jsx(Nm, { size: "sm", className: "h-8", - onClick: async () => { - await IL(`${RL()}/user-add-ipv6-subscription/enable/${e.original.id}`, {}); - hN.success("IPv6 account enabled and synced"); - p(); - }, - children: "Enable and sync", - }), - Q.jsx(Nm, { - size: "sm", - variant: "outline", - className: "h-8", - onClick: async () => { - await IL(`${RL()}/user-add-ipv6-subscription/sync-password/${e.original.id}`, {}); - hN.success("Password synced"); - p(); - }, - children: "Sync password", + onClick: () => x(e.original), + children: i, }), + t && + Q.jsx(Nm, { + size: "sm", + variant: "outline", + className: "h-8", + onClick: async () => { + await IL(`${RL()}/user-add-ipv6-subscription/disable/${e.original.id}`, {}); + hN.success("IPv6 account disabled"); + p(); + }, + children: "Disable", + }), + t && + Q.jsx(Nm, { + size: "sm", + variant: "outline", + className: "h-8", + onClick: async () => { + await IL(`${RL()}/user-add-ipv6-subscription/sync-password/${e.original.id}`, {}); + hN.success("Password synced"); + p(); + }, + children: "Sync password", + }), ], - }), + }); + }, }), ], - [p], + [x, p], ), - w = PKt({ + S = PKt({ data: f?.data?.list ?? [], - columns: x, + columns: C, state: { columnVisibility: r, rowSelection: s, pagination: l }, getRowId: (e) => String(e.id), rowCount: f?.data?.pagination?.total ?? 0, @@ -305960,82 +305986,108 @@ function codexNativeIPv6Table() { getCoreRowModel: LKt(), getPaginationRowModel: OKt(), }); - return ( - H.useEffect(() => { - const e = _?.data?.ipv6_plan_id; - u(e && e > 0 ? String(e) : "0"); - }, [_]), - Q.jsxs("div", { - className: "space-y-4", - children: [ - Q.jsx(codexNativeSearchToolbar, { - keyword: e, - setKeyword: t, - onSearch: () => { - c((e) => ({ ...e, pageIndex: 0 })); - i(e.trim()); - }, - onReset: () => { - t(""); - i(""); - c((e) => ({ ...e, pageIndex: 0 })); - }, - refetch: p, - actions: Q.jsxs("div", { - className: "flex flex-wrap items-center gap-2", - children: [ - Q.jsx("span", { className: "text-sm text-muted-foreground", children: "IPv6Only plan" }), - Q.jsxs("div", { - className: "relative", - children: [ - Q.jsxs("select", { - className: - "flex h-8 min-w-[180px] appearance-none rounded-md border border-input bg-transparent px-3 pr-8 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", - value: d, - onChange: (e) => u(e.target.value), - children: [ - Q.jsx("option", { value: "0", children: "Not set" }), - y.map((e) => - Q.jsx("option", { value: String(e.id), children: e.name || `Plan ${e.id}` }, e.id), - ), - ], - }), - Q.jsx(m7e, { - className: - "pointer-events-none absolute right-2 top-2 h-4 w-4 text-muted-foreground/70", - }), - ], - }), - Q.jsx(Nm, { - className: "h-8", - loading: h, - onClick: async () => { - g(!0); - try { - await IL(`${RL()}/user-add-ipv6-subscription/config`, { - ipv6_plan_id: Number(d) || 0, - }); - hN.success("IPv6Only plan saved"); - await Promise.all([v(), p()]); - } finally { - g(!1); - } - }, - children: "Save plan", - }), - ], - }), + return Q.jsxs("div", { + className: "space-y-4", + children: [ + Q.jsx(codexNativeSearchToolbar, { + keyword: e, + setKeyword: t, + onSearch: () => { + c((e) => ({ ...e, pageIndex: 0 })); + i(e.trim()); + }, + onReset: () => { + t(""); + i(""); + c((e) => ({ ...e, pageIndex: 0 })); + }, + refetch: p, + }), + Q.jsx(QKt, { + table: S, + isLoading: m, + showPagination: !0, + mobilePrimaryField: "email", + mobileGridFields: ["plan_name", "status"], + }), + Q.jsx(Jet, { + open: d.open, + onOpenChange: (e) => { + e ? u((e) => ({ ...e, open: !0 })) : w(); + }, + children: Q.jsxs(ttt, { + className: "sm:max-w-md", + children: [ + Q.jsxs(ntt, { + children: [ + Q.jsx(rtt, { children: "Set IPv6 plan" }), + Q.jsx(ott, { + children: d.user ? `User: ${d.user.email || d.user.id}` : "Choose the plan for this IPv6 account.", + }), + ], + }), + Q.jsxs("div", { + className: "space-y-4 p-6", + children: [ + Q.jsxs("div", { + className: "space-y-2", + children: [ + Q.jsx("label", { className: "text-sm font-medium text-foreground", children: "IPv6 plan" }), + Q.jsxs("div", { + className: "relative", + children: [ + Q.jsxs("select", { + className: + "flex h-10 w-full appearance-none rounded-md border border-input bg-transparent px-3 pr-8 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", + value: d.planId, + onChange: (e) => u((t) => ({ ...t, planId: e.target.value })), + children: [ + Q.jsx("option", { value: "0", children: "Use default plan" }), + b.map((e) => + Q.jsx("option", { value: String(e.id), children: e.name || `Plan ${e.id}` }, e.id), + ), + ], + }), + Q.jsx(m7e, { + className: + "pointer-events-none absolute right-3 top-3.5 h-4 w-4 text-muted-foreground/70", + }), + ], + }), + ], + }), + Q.jsxs("div", { + className: "flex justify-end gap-2", + children: [ + Q.jsx(Nm, { type: "button", variant: "outline", onClick: w, children: "Cancel" }), + Q.jsx(Nm, { + type: "button", + loading: h, + onClick: async () => { + if (!d.user) return; + g(!0); + try { + await IL(`${RL()}/user-add-ipv6-subscription/enable/${d.user.id}`, { + plan_id: Number(d.planId) || 0, + }); + hN.success("IPv6 account updated"); + w(); + await p(); + } finally { + g(!1); + } + }, + children: "Save", + }), + ], + }), + ], + }), + ], }), - Q.jsx(QKt, { - table: w, - isLoading: m, - showPagination: !0, - mobilePrimaryField: "email", - mobileGridFields: ["ipv6_email", "plan_name", "status"], - }), - ], - }) - ); + }), + ], + }); } function codexNativeIPv6Page() { return codexNativePageLayout( diff --git a/frontend/theme/Nebula/assets/app.js b/frontend/theme/Nebula/assets/app.js index 24c66c0..b035346 100644 --- a/frontend/theme/Nebula/assets/app.js +++ b/frontend/theme/Nebula/assets/app.js @@ -1069,12 +1069,17 @@ saveToken(payload.auth_data); state.authToken = getStoredToken(); - // Try IPv6 login/register if email/password available - if (data.email && data.password) { + showMessage(formType === "register" ? "Account created" : "Signed in", "success"); + return loadDashboard(true).then(function () { + if (formType !== "login" || !data.email || !data.password || state.ipv6AuthToken) { + return null; + } + if (!(state.ipv6Eligibility && state.ipv6Eligibility.is_active)) { + return null; + } + var ipv6Email = data.email.replace("@", "-ipv6@"); - var ipv6Action = formType === "register" ? "/api/v1/passport/auth/register" : "/api/v1/passport/auth/login"; - - fetchJson(ipv6Action, { + return fetchJson("/api/v1/passport/auth/login", { method: "POST", auth: false, body: Object.assign({}, data, { email: ipv6Email }) @@ -1085,14 +1090,11 @@ state.ipv6AuthToken = getStoredIpv6Token(); } }).catch(function () { - // Ignore IPv6 errors as it might not be enabled yet or fail - }).finally(function() { - loadDashboard(true).then(render); + return null; + }).then(function () { + return loadDashboard(true); }); - } - - showMessage(formType === "register" ? "Account created" : "Signed in", "success"); - return loadDashboard(true); + }); }).then(render).catch(function (error) { showMessage(error.message || "Unable to continue", "error"); render(); diff --git a/internal/handler/plugin_api.go b/internal/handler/plugin_api.go index 8764366..55682cd 100644 --- a/internal/handler/plugin_api.go +++ b/internal/handler/plugin_api.go @@ -183,29 +183,39 @@ func AdminIPv6SubscriptionUsers(c *gin.Context) { } subscriptionByUserID := make(map[int]model.UserIPv6Subscription, len(userIDs)) + shadowUserIDs := make([]int, 0, len(userIDs)) if len(userIDs) > 0 { var rows []model.UserIPv6Subscription if err := database.DB.Where("user_id IN ?", userIDs).Find(&rows).Error; err == nil { for _, row := range rows { subscriptionByUserID[row.UserID] = row + if row.ShadowUserID != nil && *row.ShadowUserID > 0 { + shadowUserIDs = append(shadowUserIDs, *row.ShadowUserID) + } } } } shadowByParentID := make(map[int]model.User, len(userIDs)) + shadowByID := make(map[int]model.User, len(shadowUserIDs)) + shadowByEmail := make(map[string]model.User, len(userIDs)) if len(userIDs) > 0 { var shadowUsers []model.User - if err := database.DB.Preload("Plan").Where("parent_id IN ?", userIDs).Find(&shadowUsers).Error; err == nil { + query := database.DB.Preload("Plan") + if len(shadowUserIDs) > 0 { + query = query.Where("parent_id IN ? OR id IN ?", userIDs, shadowUserIDs) + } else { + query = query.Where("parent_id IN ?", userIDs) + } + if err := query.Find(&shadowUsers).Error; err == nil { for _, shadow := range shadowUsers { + shadowByID[shadow.ID] = shadow + shadowByEmail[strings.ToLower(shadow.Email)] = shadow if shadow.ParentID != nil { shadowByParentID[*shadow.ParentID] = shadow } } } } - shadowPlanID := parsePositiveInt( - service.GetPluginConfigString(service.PluginUserAddIPv6, "ipv6_plan_id", "0"), - 0, - ) planNames := make(map[int]string) var plans []model.Plan if err := database.DB.Select("id", "name").Find(&plans).Error; err == nil { @@ -218,6 +228,12 @@ func AdminIPv6SubscriptionUsers(c *gin.Context) { for _, user := range users { subscription, hasSubscription := subscriptionByUserID[user.ID] shadowUser, hasShadowUser := shadowByParentID[user.ID] + if !hasShadowUser && hasSubscription && subscription.ShadowUserID != nil { + shadowUser, hasShadowUser = shadowByID[*subscription.ShadowUserID] + } + if !hasShadowUser { + shadowUser, hasShadowUser = shadowByEmail[strings.ToLower(service.IPv6ShadowEmail(user.Email))] + } if !hasSubscription && hasShadowUser { subscription = model.UserIPv6Subscription{ UserID: user.ID, @@ -229,10 +245,9 @@ func AdminIPv6SubscriptionUsers(c *gin.Context) { } hasSubscription = true } - allowed := service.PluginUserAllowed(&user, user.Plan) - status := "not_allowed" - planID := shadowPlanID - planNameValue := planNames[shadowPlanID] + status := "eligible" + planID := 0 + planNameValue := "" if hasShadowUser { planID = intFromPointer(shadowUser.PlanID) planNameValue = firstString(planName(shadowUser.Plan), planNames[planID]) @@ -246,14 +261,14 @@ func AdminIPv6SubscriptionUsers(c *gin.Context) { } shadowUpdatedAt = subscription.UpdatedAt } - effectiveAllowed := allowed || hasSubscription && subscription.Allowed + effectiveAllowed := true status, statusLabel, _ := ipv6StatusPresentation(status, effectiveAllowed) list = append(list, gin.H{ "id": user.ID, "email": user.Email, "plan_id": planID, - "plan_name": firstString(planNameValue, "-"), + "plan_name": planNameValue, "allowed": effectiveAllowed, "is_active": hasSubscription && subscription.Status == "active", "status": status, @@ -463,22 +478,13 @@ func PluginUserAddIPv6Check(c *gin.Context) { return } - var plan *model.Plan - if user.PlanID != nil { - var loadedPlan model.Plan - if err := database.DB.First(&loadedPlan, *user.PlanID).Error; err == nil { - plan = &loadedPlan - } - } - var subscription model.UserIPv6Subscription hasSubscription := database.DB.Where("user_id = ?", user.ID).First(&subscription).Error == nil - allowed := service.PluginUserAllowed(user, plan) - status := "not_allowed" + status := "eligible" if hasSubscription { status = firstString(subscription.Status, "active") } - effectiveAllowed := allowed || hasSubscription && subscription.Allowed + effectiveAllowed := true status, statusLabel, reason := ipv6StatusPresentation(status, effectiveAllowed) Success(c, gin.H{ diff --git a/internal/service/plugin.go b/internal/service/plugin.go index f7bb0ae..3a7b1fb 100644 --- a/internal/service/plugin.go +++ b/internal/service/plugin.go @@ -105,6 +105,7 @@ func SyncIPv6ShadowAccountWithPlan(user *model.User, overridePlanID int) bool { ipv6User.Email = ipv6Email ipv6User.Password = user.Password + ipv6User.ParentID = &user.ID ipv6User.U = 0 ipv6User.D = 0 ipv6User.T = 0 @@ -116,6 +117,8 @@ func SyncIPv6ShadowAccountWithPlan(user *model.User, overridePlanID int) bool { ipv6User.PlanID = &overridePlanID } else if planID := parsePluginPositiveInt(GetPluginConfigString(PluginUserAddIPv6, "ipv6_plan_id", "0"), 0); planID > 0 { ipv6User.PlanID = &planID + } else if ipv6User.PlanID == nil && user.PlanID != nil { + ipv6User.PlanID = user.PlanID } if groupID := parsePluginPositiveInt(GetPluginConfigString(PluginUserAddIPv6, "ipv6_group_id", "0"), 0); groupID > 0 { ipv6User.GroupID = &groupID @@ -142,7 +145,15 @@ func DisableIPv6ShadowAccount(user *model.User) bool { ipv6Email := IPv6ShadowEmail(user.Email) var ipv6User model.User - if err := database.DB.Where("email = ? AND parent_id = ?", ipv6Email, user.ID).First(&ipv6User).Error; err != nil { + + var subscription model.UserIPv6Subscription + if err := database.DB.Where("user_id = ?", user.ID).First(&subscription).Error; err == nil && subscription.ShadowUserID != nil { + _ = database.DB.First(&ipv6User, *subscription.ShadowUserID).Error + } + if ipv6User.ID == 0 { + _ = database.DB.Where("email = ?", ipv6Email).First(&ipv6User).Error + } + if ipv6User.ID == 0 { // No shadow user found, just update subscription record syncIPv6SubscriptionRecord(user, nil, false, "disabled") return true