The cc-haha desktop client used to read its balance from
/v1/dashboard/billing/{subscription,usage}. Those endpoints honor the
token row's UnlimitedQuota flag — and device-bound tokens have that
flag set true because they are an auth mechanism, not a billing
boundary. Result: the balance pill always showed 100_000_000 USD
regardless of the user's real balance.
The right source is the user row (User.Quota / UsedQuota /
RequestCount), which is what /api/user/self surfaces to the web
dashboard. But that endpoint is UserAuth-only (session cookie / JWT),
which the desktop client doesn't carry — it holds a sk- bearer or
signs requests with its V2 device key.
This commit adds a slim sibling endpoint /api/heicode/self mounted on
TokenAuth so either sk- or V2 signature authenticates. Returns only
the fields the desktop balance pill + usage panel consume (quota,
used_quota, request_count, plus username/group/role for the title
bar) — no PII beyond what relay calls already expose. Quota numbers
go through the same QuotaPerUnit / display-type normalization that
billing.go uses, so the desktop pill and web dashboard show the same
number.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
77 lines
2.3 KiB
Go
77 lines
2.3 KiB
Go
package controller
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/heicode/manager/common"
|
|
"github.com/heicode/manager/i18n"
|
|
"github.com/heicode/manager/model"
|
|
"github.com/heicode/manager/setting/operation_setting"
|
|
)
|
|
|
|
// quotaToDisplayUnit converts a raw quota integer (DB unit) to the
|
|
// site-configured display unit (USD / CNY / tokens). Mirrors the
|
|
// conversion in billing.go so the desktop "balance pill" shows the
|
|
// same number the user sees in the web dashboard.
|
|
func quotaToDisplayUnit(raw int) float64 {
|
|
amount := float64(raw)
|
|
switch operation_setting.GetQuotaDisplayType() {
|
|
case operation_setting.QuotaDisplayTypeCNY:
|
|
return amount / common.QuotaPerUnit * operation_setting.USDExchangeRate
|
|
case operation_setting.QuotaDisplayTypeTokens:
|
|
return amount
|
|
default:
|
|
return amount / common.QuotaPerUnit
|
|
}
|
|
}
|
|
|
|
// GetHeicodeSelf serves GET /api/heicode/self.
|
|
//
|
|
// Mounted on TokenAuth so the cc-haha desktop client can read it with
|
|
// either:
|
|
// - a legacy sk- bearer (30-day transition window), or
|
|
// - a V2 device-signed request (after pair).
|
|
//
|
|
// Returns the *user-row* quota counters (not the device-token's
|
|
// UnlimitedQuota flag, which is just an auth-mechanism marker). This
|
|
// is the same data /api/user/self surfaces to the web dashboard, but
|
|
// trimmed to just the fields the desktop "balance pill" + usage panel
|
|
// need. No PII (email/social ids) so a leaked sk- can't enumerate
|
|
// account identity beyond what relay calls already expose.
|
|
func GetHeicodeSelf(c *gin.Context) {
|
|
userId := c.GetInt("id")
|
|
if userId == 0 {
|
|
c.JSON(http.StatusUnauthorized, gin.H{
|
|
"success": false,
|
|
"message": common.TranslateMessage(c, i18n.MsgUnauthorized),
|
|
})
|
|
return
|
|
}
|
|
|
|
user, err := model.GetUserById(userId, false)
|
|
if err != nil {
|
|
common.SysLog("GetHeicodeSelf: " + err.Error())
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"success": false,
|
|
"message": common.TranslateMessage(c, i18n.MsgDatabaseError),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": gin.H{
|
|
"id": user.Id,
|
|
"username": user.Username,
|
|
"display_name": user.DisplayName,
|
|
"group": user.Group,
|
|
"role": user.Role,
|
|
"quota": quotaToDisplayUnit(user.Quota),
|
|
"used_quota": quotaToDisplayUnit(user.UsedQuota),
|
|
"request_count": user.RequestCount,
|
|
},
|
|
})
|
|
}
|