Files
new-api/controller/github.go
T
QuentinHsu 3a42b6cb25 feat(web): add subscription management and admin settings UI
* feat(web): add subscription management and admin settings UI

- Add subscription management module (plans, pricing, toggle status, and related dialogs/tables with Stripe/Creem integration notes)
- Add channel affinity (rules and cache stats), Waffo integration, performance, and Grok model sections to system settings, with extended types and section registry
- Add status code mapping validation/risk warnings, upstream update hooks, and utilities for channels; add available models and sidebar module cards to user profile
- Add chat2link route and useMinimumLoadingTime, useTableCompactMode shared hooks

Made-with: Cursor

* fix: remove duplicate GenerateOAuthCode and add missing TaskBulkUpdate

- remove duplicate GenerateOAuthCode from github.go since oauth.go already has the generic version.
- add model.TaskBulkUpdate for bulk update by upstream task_id strings, fixing task_video.go build failure.

* feat(router): add chat2link and subscriptions routes

- register /chat2link page route under authenticated layout.
- register /subscriptions/ page route under authenticated layout.
- update auto-generated routeTree type definitions and route mappings.

* feat(docker): add development environment setup with Docker Compose

- Introduced docker-compose.dev.yml for local development, including services for new-api, Redis, and PostgreSQL.
- Created Dockerfile.dev for backend-only builds, optimizing the development workflow.
- Updated makefile to include new commands for starting backend services and frontend development.

* feat(web): complete i18n coverage for setup wizard and add language switcher

- wrap all hardcoded English strings in setup-wizard, database-step, usage-mode-step, and complete-step with t() calls, covering step titles, descriptions, form validation messages, and fallback strings.
- add LanguageSwitcher component to the top-right corner of the setup page so users can switch language during initial setup.
- register 25 dynamic i18n keys in static-keys.ts and provide full translations for zh/en/ja/fr/ru/vi.

* feat(i18n): internationalize default version text in workspace-switcher

- remove hardcoded 'Unknown version' default, use t('Unknown version') for i18n fallback
- add "Unknown version" translation entries across all 6 locale files (zh/en/fr/ru/ja/vi)

* feat(i18n): add full i18n coverage for channel-affinity settings page

- replace Chinese t() keys with English keys across three channel-affinity components to align with new frontend i18n conventions.
- add 51 translation entries to all 6 locale files (en/zh/ja/fr/ru/vi) covering main page, rule editor, and cache stats dialog.
- register section-registry dynamic keys in static-keys.ts.

* feat(i18n): add full i18n coverage for Waffo payment settings page

- replace Chinese i18n keys with English keys in waffo-settings-section.tsx for consistency.
- wrap previously hardcoded labels (Pay Method Type / Pay Method Name) with t().
- add 26 Waffo-related translation entries across all 6 locale files (en/zh/fr/ru/ja/vi).

* feat(i18n): add missing translations for global model settings page

- add all 6 locale translations for 3 missing t() keys in global-settings-card.
- register dynamically used 'Grok' key in static-keys.ts for i18n scanner coverage.

* feat(i18n): add full i18n coverage for Grok model settings page

- add translations in all 6 locales (en/zh/fr/ja/ru/vi) for grok-settings-card t() calls.
- cover violation fee toggle, amount input, and official docs link labels.
- include section-registry descriptionKey translation entries.

* feat(i18n): add full i18n coverage for performance settings page

- migrate all t() keys from Chinese to English to align with project conventions.
- add translations for all 6 locales (en/zh/ja/fr/ru/vi) covering disk cache,
  system monitoring, log management, and stats dashboard sections.
- remove 71 obsolete Chinese-keyed entries from every locale file.

* fix(i18n): add 116 missing English translation keys across all locales

- scan all t() calls to identify English keys used in code but absent from locale files.
- add translations for zh/en/fr/ja/ru/vi, keeping key sets and sort order consistent.
- covers system-settings, channels, models, auth, wallet and other modules.

* fix(i18n): add missing translations for log cleanup quick-select and confirm dialog

- wrap quick-select button labels (24 hours ago / 7 days ago / 30 days ago) with t().
- replace hardcoded English strings in purge confirm dialog with t() calls and date interpolation.
- add 5 new translation keys across all 6 locale files (zh/en/fr/ja/ru/vi).

* refactor(web): unify all time display with dayjs formatting

- replace all toLocaleString/toLocaleDateString/toLocaleTimeString and manual padStart concatenation with dayjs.format().
- standardize output: datetime as YYYY-MM-DD HH:mm:ss, date as YYYY-MM-DD, time as HH:mm:ss.
- add formatDateTimeStr, formatDateStr, formatTimeStr dayjs-based helpers in lib/format.ts.
- update 12 files across core utils and feature components.

* refactor(web): replace native datetime-local input with DateTimePicker in announcements

- swap browser-native datetime-local for the project's DateTimePicker component to match the UI used in log cleanup and other pages.
- convert between Date objects and ISO strings to bridge the form's string-based schema.

* refactor(web): replace native HTML elements with design system components

- replace ~35 native <button> with <Button> across pricing, profile, channels modules
- replace native <input>/<textarea>/<label> with <Input>/<Textarea>/<Label> for consistent form styling
- replace native <table> with <Table> components, <details>/<summary> with <Collapsible>
- replace decorative <hr> with <Separator> to ensure global UI consistency

* refactor(web): enhance profile components with design system consistency

- update ProfileSecurityCard to use buttons for security actions, improving accessibility and styling.
- modify AccountBindingsTab layout to a grid for better responsiveness and visual alignment.
- refactor NotificationTab to utilize icons for notification methods, enhancing user experience and clarity.

* fix(i18n): complete i18n coverage for profile page components

- wrap passkey card status badges (enabled/disabled, backup state) and last-used text with t()
- fix hardcoded button labels in security dialogs (change password, access token, delete account)
- internationalize all 2FA dialog strings (setup, disable, backup codes)
- fix email bind dialog description and button state text missing i18n
- wrap remaining hardcoded strings in notification tab and checkin calendar
- add all missing translation entries to zh.json and en.json

* fix(i18n): enhance error messages with translations for deployment access and settings

- wrap connection error messages in DeploymentAccessGuard and IoNetDeploymentSettingsSection with t() for internationalization.
- add missing translation key for "io.net model deployment is not enabled or api key missing" in all locale files (en, fr, ja, ru, vi, zh).
2026-04-10 23:42:22 +08:00

221 lines
5.1 KiB
Go

package controller
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
type GitHubOAuthResponse struct {
AccessToken string `json:"access_token"`
Scope string `json:"scope"`
TokenType string `json:"token_type"`
}
type GitHubUser struct {
Login string `json:"login"`
Name string `json:"name"`
Email string `json:"email"`
}
func getGitHubUserInfoByCode(code string) (*GitHubUser, error) {
if code == "" {
return nil, errors.New("无效的参数")
}
values := map[string]string{"client_id": common.GitHubClientId, "client_secret": common.GitHubClientSecret, "code": code}
jsonData, err := json.Marshal(values)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", "https://github.com/login/oauth/access_token", bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
client := http.Client{
Timeout: 20 * time.Second,
}
res, err := client.Do(req)
if err != nil {
common.SysLog(err.Error())
return nil, errors.New("无法连接至 GitHub 服务器,请稍后重试!")
}
defer res.Body.Close()
var oAuthResponse GitHubOAuthResponse
err = json.NewDecoder(res.Body).Decode(&oAuthResponse)
if err != nil {
return nil, err
}
req, err = http.NewRequest("GET", "https://api.github.com/user", nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", oAuthResponse.AccessToken))
res2, err := client.Do(req)
if err != nil {
common.SysLog(err.Error())
return nil, errors.New("无法连接至 GitHub 服务器,请稍后重试!")
}
defer res2.Body.Close()
var githubUser GitHubUser
err = json.NewDecoder(res2.Body).Decode(&githubUser)
if err != nil {
return nil, err
}
if githubUser.Login == "" {
return nil, errors.New("返回值非法,用户字段为空,请稍后重试!")
}
return &githubUser, nil
}
func GitHubOAuth(c *gin.Context) {
session := sessions.Default(c)
state := c.Query("state")
if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) {
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"message": "state is empty or not same",
})
return
}
username := session.Get("username")
if username != nil {
GitHubBind(c)
return
}
if !common.GitHubOAuthEnabled {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "管理员未开启通过 GitHub 登录以及注册",
})
return
}
code := c.Query("code")
githubUser, err := getGitHubUserInfoByCode(code)
if err != nil {
common.ApiError(c, err)
return
}
user := model.User{
GitHubId: githubUser.Login,
}
// IsGitHubIdAlreadyTaken is unscoped
if model.IsGitHubIdAlreadyTaken(user.GitHubId) {
// FillUserByGitHubId is scoped
err := user.FillUserByGitHubId()
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
// if user.Id == 0 , user has been deleted
if user.Id == 0 {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "用户已注销",
})
return
}
} else {
if common.RegisterEnabled {
user.Username = "github_" + strconv.Itoa(model.GetMaxUserId()+1)
if githubUser.Name != "" {
user.DisplayName = githubUser.Name
} else {
user.DisplayName = "GitHub User"
}
user.Email = githubUser.Email
user.Role = common.RoleCommonUser
user.Status = common.UserStatusEnabled
affCode := session.Get("aff")
inviterId := 0
if affCode != nil {
inviterId, _ = model.GetUserIdByAffCode(affCode.(string))
}
if err := user.Insert(inviterId); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
} else {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "管理员关闭了新用户注册",
})
return
}
}
if user.Status != common.UserStatusEnabled {
c.JSON(http.StatusOK, gin.H{
"message": "用户已被封禁",
"success": false,
})
return
}
setupLogin(&user, c)
}
func GitHubBind(c *gin.Context) {
if !common.GitHubOAuthEnabled {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "管理员未开启通过 GitHub 登录以及注册",
})
return
}
code := c.Query("code")
githubUser, err := getGitHubUserInfoByCode(code)
if err != nil {
common.ApiError(c, err)
return
}
user := model.User{
GitHubId: githubUser.Login,
}
if model.IsGitHubIdAlreadyTaken(user.GitHubId) {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "该 GitHub 账户已被绑定",
})
return
}
session := sessions.Default(c)
id := session.Get("id")
// id := c.GetInt("id") // critical bug!
user.Id = id.(int)
err = user.FillUserById()
if err != nil {
common.ApiError(c, err)
return
}
user.GitHubId = githubUser.Login
err = user.Update(false)
if err != nil {
common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "bind",
})
return
}