Files
DragonCoreSSH-NewWEB/account_renewal.go
T
2026-07-13 18:01:39 -03:00

341 lines
10 KiB
Go

package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"
)
type accountRenewPayload struct {
Username string `json:"username,omitempty"`
UUID string `json:"uuid,omitempty"`
Days int `json:"days,omitempty"`
ServerID string `json:"server_id,omitempty"`
}
func renewalDays(owner string, requested int) int {
if u, ok := adminUsers.get(owner); ok && normalizeQuotaMode(u.QuotaMode) == QuotaModeCredit {
return 31
}
if requested == 0 {
return 30
}
return requested
}
func renewalExpiry(existing *time.Time, days int) time.Time {
base := time.Now()
if existing != nil && existing.After(base) {
base = *existing
}
return base.AddDate(0, 0, days)
}
func jsonInt(value interface{}) int {
switch value := value.(type) {
case int:
return value
case int64:
return int(value)
case float64:
return int(value)
case json.Number:
result, _ := strconv.Atoi(value.String())
return result
default:
result, _ := strconv.ParseFloat(fmt.Sprint(value), 64)
return int(result)
}
}
func handleRenewSSHUser(store *Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
var p accountRenewPayload
dec := json.NewDecoder(http.MaxBytesReader(w, r.Body, 8*1024))
dec.DisallowUnknownFields()
if err := dec.Decode(&p); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
p.Username = strings.TrimSpace(p.Username)
if err := validateAccountUsername(p.Username); err != nil {
http.Error(w, "invalid username", http.StatusBadRequest)
return
}
if p.Days < 0 || p.Days > 3650 {
http.Error(w, "days must be between 1 and 3650", http.StatusBadRequest)
return
}
ctx := r.Context()
sess := sessionFromCtx(ctx)
if sess != nil && sess.Role == RoleReseller {
quotaUnlock := lockResellerQuota(sess.Username)
defer quotaUnlock()
}
if ms, remote, err := managedServerFromID(ctx, store, p.ServerID); err != nil {
writeManagedServerSelectionError(w, err)
return
} else if remote {
row, exists, infoErr := remoteSSHUserInfo(ctx, ms, p.Username)
if infoErr != nil {
http.Error(w, "could not verify remote account", http.StatusBadGateway)
return
}
if !exists {
http.Error(w, "SSH account not found", http.StatusNotFound)
return
}
owner := strings.TrimSpace(fmt.Sprint(row["owner_username"]))
charged, cost := false, 0
if sess != nil && sess.Role == RoleReseller {
if owner != sess.Username {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
cost = resellerProvisionCost(jsonInt(row["max_connections"]))
charged, infoErr = reserveResellerProvisionCredits(ctx, store, owner, cost, "renew-ssh:"+p.Username)
if infoErr != nil {
writeResellerProvisionError(w, infoErr)
return
}
p.Days = renewalDays(owner, p.Days)
}
if sess != nil && sess.Role == RoleReseller {
if syncErr := syncOwnerChainToManagedServer(ctx, ms, owner); syncErr != nil {
if charged {
refundResellerProvisionCredits(ctx, store, owner, cost, "renew-ssh:"+p.Username)
}
http.Error(w, "could not synchronize reseller state with the remote server", http.StatusBadGateway)
return
}
}
p.ServerID = ""
body, _ := json.Marshal(p)
status, data, contentType, proxyErr := proxyManagedServer(ctx, ms, http.MethodPost, "/api/users/renew", body, "application/json")
if proxyErr != nil || status < 200 || status >= 300 {
if charged {
refundResellerProvisionCredits(ctx, store, owner, cost, "renew-ssh:"+p.Username)
}
if proxyErr != nil {
writeBadGatewayError(w, "renew SSH account on managed server", proxyErr)
return
}
}
writeProxyResponse(w, status, data, contentType)
return
}
state, ok := userMgr.Get(p.Username)
if !ok {
http.Error(w, "SSH account not found", http.StatusNotFound)
return
}
state.mu.Lock()
cfg := state.Cfg
existingExpiry := state.ExpiresAt
state.mu.Unlock()
charged, cost := false, 0
if sess != nil && sess.Role == RoleReseller {
if cfg.OwnerUsername != sess.Username {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
cost = resellerProvisionCost(cfg.MaxConnections)
var creditErr error
charged, creditErr = reserveResellerProvisionCredits(ctx, store, sess.Username, cost, "renew-ssh:"+p.Username)
if creditErr != nil {
writeResellerProvisionError(w, creditErr)
return
}
p.Days = renewalDays(sess.Username, p.Days)
}
if p.Days == 0 {
p.Days = 30
}
next := renewalExpiry(existingExpiry, p.Days)
cfg.ExpiresAt = next.UTC().Format(time.RFC3339)
if err := store.UpsertUser(ctx, cfg); err != nil {
if charged {
refundResellerProvisionCredits(ctx, store, cfg.OwnerUsername, cost, "renew-ssh:"+p.Username)
}
http.Error(w, "database error", http.StatusInternalServerError)
return
}
userMgr.DisconnectUser(p.Username)
reloadUsersFromDB(ctx, store)
if sess != nil {
_ = store.appendResellerAudit(ctx, sess.Username, cfg.OwnerUsername, "renewed SSH account",
fmt.Sprintf("account=%s days=%d", p.Username, p.Days))
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "expires_at": next})
}
}
func handleRenewXrayClient(store *Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
var p accountRenewPayload
dec := json.NewDecoder(http.MaxBytesReader(w, r.Body, 8*1024))
dec.DisallowUnknownFields()
if err := dec.Decode(&p); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
p.UUID = strings.TrimSpace(p.UUID)
if _, err := parseUUID(p.UUID); err != nil {
http.Error(w, "invalid UUID", http.StatusBadRequest)
return
}
if p.Days < 0 || p.Days > 3650 {
http.Error(w, "days must be between 1 and 3650", http.StatusBadRequest)
return
}
ctx := r.Context()
sess := sessionFromCtx(ctx)
if sess != nil && sess.Role == RoleReseller {
quotaUnlock := lockResellerQuota(sess.Username)
defer quotaUnlock()
}
if ms, remote, err := managedServerFromID(ctx, store, p.ServerID); err != nil {
writeManagedServerSelectionError(w, err)
return
} else if remote {
row, exists, infoErr := remoteXrayClientInfo(ctx, ms, p.UUID)
if infoErr != nil {
http.Error(w, "could not verify remote account", http.StatusBadGateway)
return
}
if !exists {
http.Error(w, "Xray account not found", http.StatusNotFound)
return
}
owner := strings.TrimSpace(fmt.Sprint(row["owner_username"]))
charged, cost := false, 0
if sess != nil && sess.Role == RoleReseller {
if owner != sess.Username {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
cost = resellerProvisionCost(jsonInt(row["max_conns"]))
charged, infoErr = reserveResellerProvisionCredits(ctx, store, owner, cost, "renew-xray:"+p.UUID)
if infoErr != nil {
writeResellerProvisionError(w, infoErr)
return
}
p.Days = renewalDays(owner, p.Days)
}
if sess != nil && sess.Role == RoleReseller {
if syncErr := syncOwnerChainToManagedServer(ctx, ms, owner); syncErr != nil {
if charged {
refundResellerProvisionCredits(ctx, store, owner, cost, "renew-xray:"+p.UUID)
}
http.Error(w, "could not synchronize reseller state with the remote server", http.StatusBadGateway)
return
}
}
p.ServerID = ""
body, _ := json.Marshal(p)
status, data, contentType, proxyErr := proxyManagedServer(ctx, ms, http.MethodPost, "/api/xray/clients/renew", body, "application/json")
if proxyErr != nil || status < 200 || status >= 300 {
if charged {
refundResellerProvisionCredits(ctx, store, owner, cost, "renew-xray:"+p.UUID)
}
if proxyErr != nil {
writeBadGatewayError(w, "renew Xray account on managed server", proxyErr)
return
}
}
writeProxyResponse(w, status, data, contentType)
return
}
meta, err := store.GetXrayClientMeta(ctx, p.UUID)
if err != nil {
http.Error(w, "Xray account not found", http.StatusNotFound)
return
}
charged, cost := false, 0
if sess != nil && sess.Role == RoleReseller {
if meta.OwnerUsername != sess.Username {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
cost = resellerProvisionCost(meta.MaxConns)
var creditErr error
charged, creditErr = reserveResellerProvisionCredits(ctx, store, sess.Username, cost, "renew-xray:"+p.UUID)
if creditErr != nil {
writeResellerProvisionError(w, creditErr)
return
}
p.Days = renewalDays(sess.Username, p.Days)
}
if p.Days == 0 {
p.Days = 30
}
next := renewalExpiry(meta.ExpiresAt, p.Days)
meta.ExpiresAt = &next
if err := store.UpsertXrayClientMeta(ctx, *meta); err != nil {
if charged {
refundResellerProvisionCredits(ctx, store, meta.OwnerUsername, cost, "renew-xray:"+p.UUID)
}
http.Error(w, "database error", http.StatusInternalServerError)
return
}
runtimeWarning := ""
if meta.OwnerUsername != "" {
if runtimeErr := restoreOwnerXrayClients(ctx, store, meta.OwnerUsername); runtimeErr != nil {
log.Printf("restore renewed Xray account %s: %v", p.UUID, runtimeErr)
runtimeWarning = "The account was renewed, but Xray could not restore it yet. Check the Xray service."
}
} else if err := ensureXrayClientPresent(*meta); err != nil {
log.Printf("restore renewed Xray account %s: %v", p.UUID, err)
runtimeWarning = "The account was renewed, but Xray could not restore it yet. Check the Xray service."
}
if sess != nil {
_ = store.appendResellerAudit(ctx, sess.Username, meta.OwnerUsername, "renewed Xray account",
fmt.Sprintf("uuid=%s days=%d", p.UUID, p.Days))
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "expires_at": next, "runtime_warning": runtimeWarning})
}
}
func ensureXrayClientPresent(meta XrayClientMeta) error {
inbounds, err := xrayMgr.ListInbounds()
if err != nil {
return err
}
for _, inbound := range inbounds {
if inbound.Tag != meta.InboundTag {
continue
}
for _, client := range inbound.Clients {
if client.UUID == meta.UUID {
return nil
}
}
email := strings.TrimSpace(meta.Email)
if email == "" {
email = meta.UUID
}
if err := xrayMgr.AddXrayClient(meta.InboundTag, meta.UUID, email); err != nil {
return err
}
xrayMgr.restartIfExternalRunning()
return nil
}
return fmt.Errorf("inbound %s no longer exists", meta.InboundTag)
}