package app import ( transportcfgpkg "selective-vpn-api/app/transportcfg" "strings" "time" ) func createSingBoxProfileLocked(st *singBoxProfilesState, req SingBoxProfileCreateRequest) (SingBoxProfile, string, error) { mode := SingBoxProfileModeTyped if strings.TrimSpace(string(req.Mode)) != "" { parsed, ok := normalizeSingBoxProfileMode(req.Mode) if !ok { return SingBoxProfile{}, singBoxProfileCodeBadMode, errSingBoxProfileMode() } mode = parsed } id := sanitizeID(req.ID) if id == "" { id = deriveSingBoxProfileID(req.Name, req.Protocol, st.Items) } if id == "" { return SingBoxProfile{}, singBoxProfileCodeBadID, errSingBoxProfileID() } if findSingBoxProfileIndex(st.Items, id) >= 0 { return SingBoxProfile{}, singBoxProfileCodeIDConflict, errSingBoxProfileExists() } enabled := true if req.Enabled != nil { enabled = *req.Enabled } now := time.Now().UTC().Format(time.RFC3339) item := SingBoxProfile{ ID: id, Name: strings.TrimSpace(req.Name), Mode: mode, Protocol: normalizeSingBoxProtocol(req.Protocol), Enabled: enabled, SchemaVersion: normalizeSingBoxSchemaVersion(req.SchemaVersion), ProfileRevision: 1, RenderRevision: 0, Typed: cloneMapDeep(req.Typed), RawConfig: cloneMapDeep(req.RawConfig), Meta: cloneMapDeep(req.Meta), CreatedAt: now, UpdatedAt: now, } if item.Name == "" { item.Name = id } secretRes, err := applySingBoxSecretsPatch(id, false, req.Secrets) if err != nil { return SingBoxProfile{}, singBoxProfileCodeSecretsFailed, err } item.HasSecrets = secretRes.HasSecrets item.SecretsMasked = secretRes.Masked st.Items = append(st.Items, item) ensureSingBoxProfilesActiveID(st) st.Revision++ return item, "", nil } func patchSingBoxProfile(cur SingBoxProfile, req SingBoxProfilePatchRequest) (SingBoxProfile, singBoxSecretsPatchResult, bool, string, error) { next := cur changed := false now := time.Now().UTC().Format(time.RFC3339) if req.Name != nil { v := strings.TrimSpace(*req.Name) if v == "" { v = next.ID } if next.Name != v { next.Name = v changed = true } } if req.Mode != nil { mode, ok := normalizeSingBoxProfileMode(*req.Mode) if !ok { return cur, singBoxSecretsPatchResult{}, false, singBoxProfileCodeBadMode, errSingBoxProfileMode() } if next.Mode != mode { next.Mode = mode changed = true } } if req.Protocol != nil { v := normalizeSingBoxProtocol(*req.Protocol) if next.Protocol != v { next.Protocol = v changed = true } } if req.Enabled != nil && next.Enabled != *req.Enabled { next.Enabled = *req.Enabled changed = true } if req.SchemaVersion != nil { v := normalizeSingBoxSchemaVersion(*req.SchemaVersion) if next.SchemaVersion != v { next.SchemaVersion = v changed = true } } if req.Typed != nil { next.Typed = cloneMapDeep(req.Typed) changed = true } if req.RawConfig != nil { next.RawConfig = cloneMapDeep(req.RawConfig) changed = true } if req.Meta != nil { next.Meta = cloneMapDeep(req.Meta) changed = true } secretRes := singBoxSecretsPatchResult{ HasSecrets: next.HasSecrets, Masked: transportcfgpkg.CloneStringMap(next.SecretsMasked), } if req.ClearSecrets || req.Secrets != nil { var err error secretRes, err = applySingBoxSecretsPatch(next.ID, req.ClearSecrets, req.Secrets) if err != nil { return cur, singBoxSecretsPatchResult{}, false, singBoxProfileCodeSecretsFailed, err } next.HasSecrets = secretRes.HasSecrets next.SecretsMasked = secretRes.Masked if secretRes.Changed { changed = true } } if !changed { return cur, secretRes, false, "", nil } next.ProfileRevision = cur.ProfileRevision + 1 next.UpdatedAt = now return next, secretRes, true, "", nil }