proxy_handler.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. package admin
  2. import (
  3. "context"
  4. "strconv"
  5. "strings"
  6. "time"
  7. "github.com/Wei-Shaw/sub2api/internal/handler/dto"
  8. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  9. "github.com/Wei-Shaw/sub2api/internal/service"
  10. "github.com/gin-gonic/gin"
  11. )
  12. // ProxyHandler handles admin proxy management
  13. type ProxyHandler struct {
  14. adminService service.AdminService
  15. }
  16. // NewProxyHandler creates a new admin proxy handler
  17. func NewProxyHandler(adminService service.AdminService) *ProxyHandler {
  18. return &ProxyHandler{
  19. adminService: adminService,
  20. }
  21. }
  22. // CreateProxyRequest represents create proxy request
  23. type CreateProxyRequest struct {
  24. Name string `json:"name" binding:"required"`
  25. Protocol string `json:"protocol" binding:"required,oneof=http https socks5 socks5h"`
  26. Host string `json:"host" binding:"required"`
  27. Port int `json:"port" binding:"required,min=1,max=65535"`
  28. Username string `json:"username"`
  29. Password string `json:"password"`
  30. ExpiresAt *int64 `json:"expires_at"`
  31. FallbackMode string `json:"fallback_mode" binding:"omitempty,oneof=none proxy direct"`
  32. BackupProxyID *int64 `json:"backup_proxy_id"`
  33. ExpiryWarnDays int `json:"expiry_warn_days" binding:"omitempty,min=0"`
  34. }
  35. // UpdateProxyRequest represents update proxy request
  36. type UpdateProxyRequest struct {
  37. Name string `json:"name"`
  38. Protocol string `json:"protocol" binding:"omitempty,oneof=http https socks5 socks5h"`
  39. Host string `json:"host"`
  40. Port int `json:"port" binding:"omitempty,min=1,max=65535"`
  41. Username string `json:"username"`
  42. Password string `json:"password"`
  43. Status string `json:"status" binding:"omitempty,oneof=active inactive"`
  44. ExpiresAt dto.NullableInt64Field `json:"expires_at"`
  45. FallbackMode string `json:"fallback_mode" binding:"omitempty,oneof=none proxy direct"`
  46. BackupProxyID dto.NullableInt64Field `json:"backup_proxy_id"`
  47. ExpiryWarnDays *int `json:"expiry_warn_days" binding:"omitempty,min=0"`
  48. }
  49. // List handles listing all proxies with pagination
  50. // GET /api/v1/admin/proxies
  51. func (h *ProxyHandler) List(c *gin.Context) {
  52. page, pageSize := response.ParsePagination(c)
  53. protocol := c.Query("protocol")
  54. status := c.Query("status")
  55. search := c.Query("search")
  56. sortBy := c.DefaultQuery("sort_by", "id")
  57. sortOrder := c.DefaultQuery("sort_order", "desc")
  58. // 标准化和验证 search 参数
  59. search = strings.TrimSpace(search)
  60. if len(search) > 100 {
  61. search = search[:100]
  62. }
  63. proxies, total, err := h.adminService.ListProxiesWithAccountCount(c.Request.Context(), page, pageSize, protocol, status, search, sortBy, sortOrder)
  64. if err != nil {
  65. response.ErrorFrom(c, err)
  66. return
  67. }
  68. out := make([]dto.AdminProxyWithAccountCount, 0, len(proxies))
  69. for i := range proxies {
  70. out = append(out, *dto.ProxyWithAccountCountFromServiceAdmin(&proxies[i]))
  71. }
  72. response.Paginated(c, out, total, page, pageSize)
  73. }
  74. // GetAll handles getting all active proxies without pagination
  75. // GET /api/v1/admin/proxies/all
  76. // Optional query param: with_count=true to include account count per proxy
  77. func (h *ProxyHandler) GetAll(c *gin.Context) {
  78. withCount := c.Query("with_count") == "true"
  79. if withCount {
  80. proxies, err := h.adminService.GetAllProxiesWithAccountCount(c.Request.Context())
  81. if err != nil {
  82. response.ErrorFrom(c, err)
  83. return
  84. }
  85. out := make([]dto.AdminProxyWithAccountCount, 0, len(proxies))
  86. for i := range proxies {
  87. out = append(out, *dto.ProxyWithAccountCountFromServiceAdmin(&proxies[i]))
  88. }
  89. response.Success(c, out)
  90. return
  91. }
  92. proxies, err := h.adminService.GetAllProxies(c.Request.Context())
  93. if err != nil {
  94. response.ErrorFrom(c, err)
  95. return
  96. }
  97. out := make([]dto.AdminProxy, 0, len(proxies))
  98. for i := range proxies {
  99. out = append(out, *dto.ProxyFromServiceAdmin(&proxies[i]))
  100. }
  101. response.Success(c, out)
  102. }
  103. // GetByID handles getting a proxy by ID
  104. // GET /api/v1/admin/proxies/:id
  105. func (h *ProxyHandler) GetByID(c *gin.Context) {
  106. proxyID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  107. if err != nil {
  108. response.BadRequest(c, "Invalid proxy ID")
  109. return
  110. }
  111. proxy, err := h.adminService.GetProxy(c.Request.Context(), proxyID)
  112. if err != nil {
  113. response.ErrorFrom(c, err)
  114. return
  115. }
  116. response.Success(c, dto.ProxyFromServiceAdmin(proxy))
  117. }
  118. // Create handles creating a new proxy
  119. // POST /api/v1/admin/proxies
  120. func (h *ProxyHandler) Create(c *gin.Context) {
  121. var req CreateProxyRequest
  122. if err := c.ShouldBindJSON(&req); err != nil {
  123. response.BadRequest(c, "Invalid request: "+err.Error())
  124. return
  125. }
  126. executeAdminIdempotentJSON(c, "admin.proxies.create", req, service.DefaultWriteIdempotencyTTL(), func(ctx context.Context) (any, error) {
  127. var expiresAt *time.Time
  128. if req.ExpiresAt != nil && *req.ExpiresAt > 0 {
  129. t := time.Unix(*req.ExpiresAt, 0).UTC()
  130. expiresAt = &t
  131. }
  132. proxy, err := h.adminService.CreateProxy(ctx, &service.CreateProxyInput{
  133. Name: strings.TrimSpace(req.Name),
  134. Protocol: strings.TrimSpace(req.Protocol),
  135. Host: strings.TrimSpace(req.Host),
  136. Port: req.Port,
  137. Username: strings.TrimSpace(req.Username),
  138. Password: strings.TrimSpace(req.Password),
  139. ExpiresAt: expiresAt,
  140. FallbackMode: strings.TrimSpace(req.FallbackMode),
  141. BackupProxyID: req.BackupProxyID,
  142. ExpiryWarnDays: req.ExpiryWarnDays,
  143. })
  144. if err != nil {
  145. return nil, err
  146. }
  147. return dto.ProxyFromServiceAdmin(proxy), nil
  148. })
  149. }
  150. // Update handles updating a proxy
  151. // PUT /api/v1/admin/proxies/:id
  152. func (h *ProxyHandler) Update(c *gin.Context) {
  153. proxyID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  154. if err != nil {
  155. response.BadRequest(c, "Invalid proxy ID")
  156. return
  157. }
  158. var req UpdateProxyRequest
  159. if err := c.ShouldBindJSON(&req); err != nil {
  160. response.BadRequest(c, "Invalid request: "+err.Error())
  161. return
  162. }
  163. var expiresAt *time.Time
  164. if req.ExpiresAt.Value != nil && *req.ExpiresAt.Value > 0 {
  165. t := time.Unix(*req.ExpiresAt.Value, 0).UTC()
  166. expiresAt = &t
  167. }
  168. proxy, err := h.adminService.UpdateProxy(c.Request.Context(), proxyID, &service.UpdateProxyInput{
  169. Name: strings.TrimSpace(req.Name),
  170. Protocol: strings.TrimSpace(req.Protocol),
  171. Host: strings.TrimSpace(req.Host),
  172. Port: req.Port,
  173. Username: strings.TrimSpace(req.Username),
  174. Password: strings.TrimSpace(req.Password),
  175. Status: strings.TrimSpace(req.Status),
  176. ExpiresAt: expiresAt,
  177. ClearExpiresAt: req.ExpiresAt.Set && expiresAt == nil,
  178. FallbackMode: strings.TrimSpace(req.FallbackMode),
  179. BackupProxyID: req.BackupProxyID.Value,
  180. ClearBackupID: req.BackupProxyID.Set && req.BackupProxyID.Value == nil,
  181. ExpiryWarnDays: req.ExpiryWarnDays,
  182. })
  183. if err != nil {
  184. response.ErrorFrom(c, err)
  185. return
  186. }
  187. response.Success(c, dto.ProxyFromServiceAdmin(proxy))
  188. }
  189. // Delete handles deleting a proxy
  190. // DELETE /api/v1/admin/proxies/:id
  191. func (h *ProxyHandler) Delete(c *gin.Context) {
  192. proxyID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  193. if err != nil {
  194. response.BadRequest(c, "Invalid proxy ID")
  195. return
  196. }
  197. err = h.adminService.DeleteProxy(c.Request.Context(), proxyID)
  198. if err != nil {
  199. response.ErrorFrom(c, err)
  200. return
  201. }
  202. response.Success(c, gin.H{"message": "Proxy deleted successfully"})
  203. }
  204. // BatchDelete handles batch deleting proxies
  205. // POST /api/v1/admin/proxies/batch-delete
  206. func (h *ProxyHandler) BatchDelete(c *gin.Context) {
  207. type BatchDeleteRequest struct {
  208. IDs []int64 `json:"ids" binding:"required,min=1"`
  209. }
  210. var req BatchDeleteRequest
  211. if err := c.ShouldBindJSON(&req); err != nil {
  212. response.BadRequest(c, "Invalid request: "+err.Error())
  213. return
  214. }
  215. result, err := h.adminService.BatchDeleteProxies(c.Request.Context(), req.IDs)
  216. if err != nil {
  217. response.ErrorFrom(c, err)
  218. return
  219. }
  220. response.Success(c, result)
  221. }
  222. // Test handles testing proxy connectivity
  223. // POST /api/v1/admin/proxies/:id/test
  224. func (h *ProxyHandler) Test(c *gin.Context) {
  225. proxyID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  226. if err != nil {
  227. response.BadRequest(c, "Invalid proxy ID")
  228. return
  229. }
  230. result, err := h.adminService.TestProxy(c.Request.Context(), proxyID)
  231. if err != nil {
  232. response.ErrorFrom(c, err)
  233. return
  234. }
  235. response.Success(c, result)
  236. }
  237. // CheckQuality handles checking proxy quality across common AI targets.
  238. // POST /api/v1/admin/proxies/:id/quality-check
  239. func (h *ProxyHandler) CheckQuality(c *gin.Context) {
  240. proxyID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  241. if err != nil {
  242. response.BadRequest(c, "Invalid proxy ID")
  243. return
  244. }
  245. result, err := h.adminService.CheckProxyQuality(c.Request.Context(), proxyID)
  246. if err != nil {
  247. response.ErrorFrom(c, err)
  248. return
  249. }
  250. response.Success(c, result)
  251. }
  252. // GetStats handles getting proxy statistics
  253. // GET /api/v1/admin/proxies/:id/stats
  254. func (h *ProxyHandler) GetStats(c *gin.Context) {
  255. proxyID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  256. if err != nil {
  257. response.BadRequest(c, "Invalid proxy ID")
  258. return
  259. }
  260. // Return mock data for now
  261. _ = proxyID
  262. response.Success(c, gin.H{
  263. "total_accounts": 0,
  264. "active_accounts": 0,
  265. "total_requests": 0,
  266. "success_rate": 100.0,
  267. "average_latency": 0,
  268. })
  269. }
  270. // GetProxyAccounts handles getting accounts using a proxy
  271. // GET /api/v1/admin/proxies/:id/accounts
  272. func (h *ProxyHandler) GetProxyAccounts(c *gin.Context) {
  273. proxyID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  274. if err != nil {
  275. response.BadRequest(c, "Invalid proxy ID")
  276. return
  277. }
  278. accounts, err := h.adminService.GetProxyAccounts(c.Request.Context(), proxyID)
  279. if err != nil {
  280. response.ErrorFrom(c, err)
  281. return
  282. }
  283. out := make([]dto.ProxyAccountSummary, 0, len(accounts))
  284. for i := range accounts {
  285. out = append(out, *dto.ProxyAccountSummaryFromService(&accounts[i]))
  286. }
  287. response.Success(c, out)
  288. }
  289. // BatchCreateProxyItem represents a single proxy in batch create request
  290. type BatchCreateProxyItem struct {
  291. Protocol string `json:"protocol" binding:"required,oneof=http https socks5 socks5h"`
  292. Host string `json:"host" binding:"required"`
  293. Port int `json:"port" binding:"required,min=1,max=65535"`
  294. Username string `json:"username"`
  295. Password string `json:"password"`
  296. }
  297. // BatchCreateRequest represents batch create proxies request
  298. type BatchCreateRequest struct {
  299. Proxies []BatchCreateProxyItem `json:"proxies" binding:"required,min=1"`
  300. }
  301. // BatchCreate handles batch creating proxies
  302. // POST /api/v1/admin/proxies/batch
  303. func (h *ProxyHandler) BatchCreate(c *gin.Context) {
  304. var req BatchCreateRequest
  305. if err := c.ShouldBindJSON(&req); err != nil {
  306. response.BadRequest(c, "Invalid request: "+err.Error())
  307. return
  308. }
  309. created := 0
  310. skipped := 0
  311. for _, item := range req.Proxies {
  312. // Trim all string fields
  313. host := strings.TrimSpace(item.Host)
  314. protocol := strings.TrimSpace(item.Protocol)
  315. username := strings.TrimSpace(item.Username)
  316. password := strings.TrimSpace(item.Password)
  317. // Check for duplicates (same host, port, username, password)
  318. exists, err := h.adminService.CheckProxyExists(c.Request.Context(), host, item.Port, username, password)
  319. if err != nil {
  320. response.ErrorFrom(c, err)
  321. return
  322. }
  323. if exists {
  324. skipped++
  325. continue
  326. }
  327. // Create proxy with default name
  328. _, err = h.adminService.CreateProxy(c.Request.Context(), &service.CreateProxyInput{
  329. Name: "default",
  330. Protocol: protocol,
  331. Host: host,
  332. Port: item.Port,
  333. Username: username,
  334. Password: password,
  335. })
  336. if err != nil {
  337. // If creation fails due to duplicate, count as skipped
  338. skipped++
  339. continue
  340. }
  341. created++
  342. }
  343. response.Success(c, gin.H{
  344. "created": created,
  345. "skipped": skipped,
  346. })
  347. }