Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions controller/cli_login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package controller

import (
"errors"
"net/http"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/i18n"

"github.com/gin-gonic/gin"
)

// CliLoginExchange swaps the caller's session for a fresh access token,
// for use by the newapi CLI's browser-login flow. The browser-side page
// arrives here with a valid session cookie (the user just signed in or
// was already signed in) and a state nonce that the CLI generated. We
// echo the state back so the loopback endpoint can verify the response
// came from the originating CLI invocation; we do NOT validate state
// server-side — that is the CLI's job.
func CliLoginExchange(c *gin.Context) {
id := c.GetInt("id")
if id == 0 {
// session missing — defensive: session-auth middleware on the
// /api/user group should have already rejected this.
common.ApiErrorI18n(c, i18n.MsgAuthNotLoggedIn)
return
}

var req struct {
State string `json:"state" binding:"required,max=128"`
}
if err := c.ShouldBindJSON(&req); err != nil {
common.ApiError(c, err)
return
}

token, err := issueAccessTokenForUser(id)
if err != nil {
switch {
case errors.Is(err, errIssueAccessTokenGenerateFailed):
common.ApiErrorI18n(c, i18n.MsgGenerateFailed)
case errors.Is(err, errIssueAccessTokenDuplicate):
common.ApiErrorI18n(c, i18n.MsgUuidDuplicate)
default:
common.ApiError(c, err)
}
return
}

c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": gin.H{
"access_token": token,
"user_id": id,
"state": req.State,
},
})
}
52 changes: 41 additions & 11 deletions controller/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,37 +287,67 @@ func GetUser(c *gin.Context) {
return
}

func GenerateAccessToken(c *gin.Context) {
id := c.GetInt("id")
user, err := model.GetUserById(id, true)
// Sentinel errors returned by issueAccessTokenForUser so that callers
// can map them to the appropriate i18n response without the helper
// depending on the gin context.
var (
errIssueAccessTokenGenerateFailed = errors.New("failed to generate access token key")
errIssueAccessTokenDuplicate = errors.New("generated access token duplicates an existing one")
)

// issueAccessTokenForUser issues a fresh access token for the given
// user and returns the bare token string. Used by both
// GenerateAccessToken (GET /api/user/token) and CliLoginExchange
// (POST /api/user/cli-login/exchange, added in a follow-up commit).
//
// Errors are returned as-is from model.GetUserById / user.Update, or
// as one of the sentinel errors above when key generation fails or
// the freshly minted key collides with an existing one. Callers are
// responsible for translating these into HTTP responses.
func issueAccessTokenForUser(userID int) (string, error) {
user, err := model.GetUserById(userID, true)
if err != nil {
common.ApiError(c, err)
return
return "", err
}
// get rand int 28-32
randI := common.GetRandomInt(4)
key, err := common.GenerateRandomKey(29 + randI)
if err != nil {
common.ApiErrorI18n(c, i18n.MsgGenerateFailed)
common.SysLog("failed to generate key: " + err.Error())
return
return "", errIssueAccessTokenGenerateFailed
}
user.SetAccessToken(key)

if model.DB.Where("access_token = ?", user.AccessToken).First(user).RowsAffected != 0 {
common.ApiErrorI18n(c, i18n.MsgUuidDuplicate)
return
return "", errIssueAccessTokenDuplicate
}

if err := user.Update(false); err != nil {
common.ApiError(c, err)
return "", err
}

return user.GetAccessToken(), nil
}

func GenerateAccessToken(c *gin.Context) {
id := c.GetInt("id")
token, err := issueAccessTokenForUser(id)
if err != nil {
switch {
case errors.Is(err, errIssueAccessTokenGenerateFailed):
common.ApiErrorI18n(c, i18n.MsgGenerateFailed)
case errors.Is(err, errIssueAccessTokenDuplicate):
common.ApiErrorI18n(c, i18n.MsgUuidDuplicate)
default:
common.ApiError(c, err)
}
return
}

c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": user.AccessToken,
"data": token,
})
return
}
Expand Down
1 change: 1 addition & 0 deletions router/api-router.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ func SetApiRouter(router *gin.Engine) {
selfRoute.PUT("/self", controller.UpdateSelf)
selfRoute.DELETE("/self", controller.DeleteSelf)
selfRoute.GET("/token", controller.GenerateAccessToken)
selfRoute.POST("/cli-login/exchange", controller.CliLoginExchange)
selfRoute.GET("/passkey", controller.PasskeyStatus)
selfRoute.POST("/passkey/register/begin", controller.PasskeyRegisterBegin)
selfRoute.POST("/passkey/register/finish", controller.PasskeyRegisterFinish)
Expand Down
2 changes: 2 additions & 0 deletions web/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import User from './pages/User';
import { AuthRedirect, PrivateRoute, AdminRoute } from './helpers';
import RegisterForm from './components/auth/RegisterForm';
import LoginForm from './components/auth/LoginForm';
import CliLogin from './components/auth/CliLogin';
import NotFound from './pages/NotFound';
import Forbidden from './pages/Forbidden';
import Setting from './pages/Setting';
Expand Down Expand Up @@ -183,6 +184,7 @@ function App() {
</Suspense>
}
/>
<Route path='/cli-login' element={<CliLogin />} />
<Route
path='/register'
element={
Expand Down
Loading