Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
a8729b5c
authored
Sep 06, 2026
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(security): require verification for access token management
parent
3e84ec0a
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
393 additions
and
130 deletions
+393
-130
controller/access_token.go
+38
-3
controller/access_token_audit_test.go
+8
-13
controller/security_enrollment_test.go
+0
-0
controller/user.go
+0
-30
router/api-router.go
+1
-1
service/security_verification.go
+16
-12
web/src/features/auth/secure-verification/types.ts
+2
-0
web/src/features/security/api.ts
+38
-20
web/src/features/security/components/__tests__/access-token-card.test.tsx
+192
-11
web/src/features/security/components/access-token-card.tsx
+10
-10
web/src/features/security/hooks/use-access-token.ts
+84
-25
web/src/lib/secure-verification.ts
+4
-5
No files found.
controller/access_token.go
View file @
a8729b5c
package
controller
import
(
"net/http"
"strconv"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/i18n"
"github.com/QuantumNous/new-api/middleware"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/service"
"github.com/gin-gonic/gin"
"strconv"
)
func
GetAccessTokenStatus
(
c
*
gin
.
Context
)
{
status
,
err
:=
model
.
GetUserAccessTokenStatus
(
c
.
GetInt
(
"id"
))
if
err
!=
nil
{
common
.
Api
Error
(
c
,
err
)
writeSecurityOperation
Error
(
c
,
err
)
return
}
common
.
ApiSuccess
(
c
,
status
)
}
func
GenerateAccessToken
(
c
*
gin
.
Context
)
{
if
middleware
.
RequireSecurityProof
(
c
,
service
.
VerificationOperation
{
Scope
:
service
.
VerificationScopeAccessTokenGenerate
})
==
nil
{
return
}
id
:=
c
.
GetInt
(
"id"
)
key
,
err
:=
common
.
GenerateRandomKey
(
29
+
common
.
GetRandomInt
(
4
))
if
err
!=
nil
{
writeSecurityOperationError
(
c
,
err
)
return
}
var
existing
int64
if
err
:=
model
.
DB
.
Model
(
&
model
.
User
{})
.
Where
(
"access_token = ?"
,
key
)
.
Count
(
&
existing
)
.
Error
;
err
!=
nil
{
writeSecurityOperationError
(
c
,
err
)
return
}
if
existing
!=
0
{
common
.
ApiErrorI18n
(
c
,
i18n
.
MsgUuidDuplicate
)
return
}
if
err
:=
model
.
UpdateUserAccessToken
(
id
,
key
);
err
!=
nil
{
writeSecurityOperationError
(
c
,
err
)
return
}
recordUserSecurityAudit
(
c
,
id
,
"access_token.generate"
,
map
[
string
]
interface
{}{
"token_ref"
:
model
.
AccessTokenFingerprint
(
key
)})
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
""
,
"data"
:
key
})
}
func
RevokeAccessToken
(
c
*
gin
.
Context
)
{
if
middleware
.
RequireSecurityProof
(
c
,
service
.
VerificationOperation
{
Scope
:
service
.
VerificationScopeAccessTokenRevoke
})
==
nil
{
return
}
ref
,
err
:=
model
.
RevokeUserAccessToken
(
c
.
GetInt
(
"id"
))
if
err
!=
nil
{
common
.
Api
Error
(
c
,
err
)
writeSecurityOperation
Error
(
c
,
err
)
return
}
if
ref
!=
""
{
...
...
controller/access_token_audit_test.go
View file @
a8729b5c
...
...
@@ -91,21 +91,16 @@ func TestAccessTokenLifecycleAndLateRequests(t *testing.T) {
assert
.
NotNil
(
t
,
status
.
CreatedAt
)
assert
.
Nil
(
t
,
status
.
LastUsedAt
,
"in-flight old requests must not mark the new generation as used"
)
assert
.
Equal
(
t
,
401
,
auditRequest
(
router
,
"GET"
,
"/api/user/token/status"
,
old
)
.
Code
)
for
_
,
method
:=
range
[]
string
{
"POST"
,
"GET"
}
{
for
_
,
method
:=
range
[]
string
{
"POST"
,
"GET"
,
"DELETE"
}
{
response
:=
auditRequest
(
router
,
method
,
"/api/user/token"
,
"new-token"
)
var
result
struct
{
Success
bool
Data
string
}
require
.
NoError
(
t
,
common
.
Unmarshal
(
response
.
Body
.
Bytes
(),
&
result
))
require
.
True
(
t
,
result
.
Success
)
require
.
GreaterOrEqual
(
t
,
len
(
result
.
Data
),
28
)
require
.
LessOrEqual
(
t
,
len
(
result
.
Data
),
32
)
assert
.
Equal
(
t
,
401
,
auditRequest
(
router
,
"GET"
,
"/api/user/token/status"
,
"new-token"
)
.
Code
)
require
.
NoError
(
t
,
model
.
UpdateUserAccessToken
(
user
.
Id
,
"new-token"
))
assert
.
Equal
(
t
,
http
.
StatusForbidden
,
response
.
Code
)
assert
.
Contains
(
t
,
response
.
Body
.
String
(),
`"code":"SECURITY_PROOF_INVALID"`
)
stored
,
err
:=
model
.
GetUserById
(
user
.
Id
,
true
)
require
.
NoError
(
t
,
err
)
assert
.
Equal
(
t
,
"new-token"
,
stored
.
GetAccessToken
(),
"a PAT cannot manage itself without a dashboard verification"
)
}
response
:=
auditRequest
(
router
,
"DELETE"
,
"/api/user/token"
,
"new-token"
)
assert
.
Contains
(
t
,
response
.
Body
.
String
(),
`"success":true`
)
_
,
err
=
model
.
RevokeUserAccessToken
(
user
.
Id
)
require
.
NoError
(
t
,
err
)
assert
.
Equal
(
t
,
401
,
auditRequest
(
router
,
"GET"
,
"/api/user/token/status"
,
"new-token"
)
.
Code
)
ref
,
err
:=
model
.
RevokeUserAccessToken
(
user
.
Id
)
require
.
NoError
(
t
,
err
)
...
...
controller/security_enrollment_test.go
View file @
a8729b5c
This diff is collapsed.
Click to expand it.
controller/user.go
View file @
a8729b5c
...
...
@@ -427,36 +427,6 @@ func GetUser(c *gin.Context) {
return
}
func
GenerateAccessToken
(
c
*
gin
.
Context
)
{
id
:=
c
.
GetInt
(
"id"
)
// 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
}
if
model
.
DB
.
Where
(
"access_token = ?"
,
key
)
.
First
(
&
model
.
User
{})
.
RowsAffected
!=
0
{
common
.
ApiErrorI18n
(
c
,
i18n
.
MsgUuidDuplicate
)
return
}
if
err
:=
model
.
UpdateUserAccessToken
(
id
,
key
);
err
!=
nil
{
common
.
ApiError
(
c
,
err
)
return
}
recordUserSecurityAudit
(
c
,
id
,
"access_token.generate"
,
map
[
string
]
interface
{}{
"token_ref"
:
model
.
AccessTokenFingerprint
(
key
)})
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
""
,
"data"
:
key
,
})
return
}
type
TransferAffQuotaRequest
struct
{
Quota
int
`json:"quota" binding:"required"`
}
...
...
router/api-router.go
View file @
a8729b5c
...
...
@@ -97,7 +97,7 @@ func SetApiRouter(router *gin.Engine) {
selfRoute
.
GET
(
"/token"
,
middleware
.
CriticalRateLimit
(),
middleware
.
UserCriticalRateLimit
(
"access-token"
),
middleware
.
DisableCache
(),
controller
.
GenerateAccessToken
)
selfRoute
.
GET
(
"/token/status"
,
middleware
.
DisableCache
(),
controller
.
GetAccessTokenStatus
)
selfRoute
.
POST
(
"/token"
,
middleware
.
CriticalRateLimit
(),
middleware
.
UserCriticalRateLimit
(
"access-token"
),
middleware
.
DisableCache
(),
controller
.
GenerateAccessToken
)
selfRoute
.
DELETE
(
"/token"
,
middleware
.
CriticalRateLimit
(),
middleware
.
DisableCache
(),
controller
.
RevokeAccessToken
)
selfRoute
.
DELETE
(
"/token"
,
middleware
.
CriticalRateLimit
(),
middleware
.
UserCriticalRateLimit
(
"access-token"
),
middleware
.
DisableCache
(),
controller
.
RevokeAccessToken
)
selfRoute
.
GET
(
"/passkey"
,
controller
.
PasskeyStatus
)
selfRoute
.
POST
(
"/passkey/register/begin"
,
middleware
.
UserCriticalRateLimit
(
"security-verification"
),
middleware
.
DisableCache
(),
controller
.
PasskeyRegisterBegin
)
selfRoute
.
POST
(
"/passkey/register/finish"
,
middleware
.
UserCriticalRateLimit
(
"security-verification"
),
middleware
.
DisableCache
(),
controller
.
PasskeyRegisterFinish
)
...
...
service/security_verification.go
View file @
a8729b5c
...
...
@@ -15,15 +15,17 @@ import (
)
const
(
VerificationMethodTwoFA
=
"2fa"
VerificationMethodPasskey
=
"passkey"
VerificationMethodPassword
=
"password"
VerificationMethodOAuth
=
"oauth"
VerificationMethodSession
=
"session"
VerificationScopeChannelKeyRead
=
"channel.key.read"
VerificationScopePasskeyRegister
=
"passkey.register"
VerificationScopePasskeyDelete
=
"passkey.delete"
VerificationScopeTwoFASetup
=
"2fa.setup"
VerificationMethodTwoFA
=
"2fa"
VerificationMethodPasskey
=
"passkey"
VerificationMethodPassword
=
"password"
VerificationMethodOAuth
=
"oauth"
VerificationMethodSession
=
"session"
VerificationScopeChannelKeyRead
=
"channel.key.read"
VerificationScopePasskeyRegister
=
"passkey.register"
VerificationScopePasskeyDelete
=
"passkey.delete"
VerificationScopeTwoFASetup
=
"2fa.setup"
VerificationScopeAccessTokenGenerate
=
"access_token.generate"
VerificationScopeAccessTokenRevoke
=
"access_token.revoke"
)
var
(
...
...
@@ -68,7 +70,8 @@ func BindVerificationOperation(operation VerificationOperation) (VerificationBin
return
VerificationBinding
{},
ErrVerificationContextInvalid
}
normalized
=
context
case
VerificationScopePasskeyRegister
,
VerificationScopePasskeyDelete
,
VerificationScopeTwoFASetup
:
case
VerificationScopePasskeyRegister
,
VerificationScopePasskeyDelete
,
VerificationScopeTwoFASetup
,
VerificationScopeAccessTokenGenerate
,
VerificationScopeAccessTokenRevoke
:
if
len
(
fields
)
!=
0
{
return
VerificationBinding
{},
ErrVerificationContextInvalid
}
...
...
@@ -142,7 +145,8 @@ func securityVerificationPolicy(scope string, state verificationAccountState) ([
}
else
if
state
.
HasPasskey
{
methods
=
[]
string
{
VerificationMethodPasskey
}
}
case
VerificationScopePasskeyRegister
,
VerificationScopeTwoFASetup
:
case
VerificationScopePasskeyRegister
,
VerificationScopeTwoFASetup
,
VerificationScopeAccessTokenGenerate
,
VerificationScopeAccessTokenRevoke
:
if
scope
==
VerificationScopeTwoFASetup
&&
state
.
HasTwoFA
{
return
nil
,
model
.
ErrTwoFAAlreadyEnabled
}
...
...
@@ -153,7 +157,7 @@ func securityVerificationPolicy(scope string, state verificationAccountState) ([
methods
=
[]
string
{
VerificationMethodPasskey
}
case
state
.
HasPassword
:
methods
=
[]
string
{
VerificationMethodPassword
}
case
state
.
WeChatEnrollment
:
case
state
.
WeChatEnrollment
&&
(
scope
==
VerificationScopePasskeyRegister
||
scope
==
VerificationScopeTwoFASetup
)
:
methods
=
[]
string
{
VerificationMethodSession
}
default
:
methods
=
[]
string
{
VerificationMethodOAuth
}
...
...
web/src/features/auth/secure-verification/types.ts
View file @
a8729b5c
...
...
@@ -27,6 +27,8 @@ export type SecurityProofScope =
|
'passkey.register'
|
'passkey.delete'
|
'2fa.setup'
|
'access_token.generate'
|
'access_token.revoke'
export
type
VerificationOperation
=
|
{
scope
:
'channel.key.read'
;
context
:
{
channel_id
:
number
}
}
...
...
web/src/features/security/api.ts
View file @
a8729b5c
...
...
@@ -16,9 +16,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import
type
{
ApiResponse
,
TwoFAStatus
}
from
'@/features/profile/types'
import
type
{
TwoFAStatus
}
from
'@/features/profile/types'
import
{
api
}
from
'@/lib/api'
import
{
authRequestOptions
,
authResult
}
from
'@/lib/secure-verification'
import
{
AuthOperationError
,
authRequestOptions
,
authResult
,
}
from
'@/lib/secure-verification'
export
interface
AccessTokenStatus
{
exists
:
boolean
...
...
@@ -28,29 +32,43 @@ export interface AccessTokenStatus {
last_used_ip
:
string
}
export
async
function
getAccessTokenStatus
():
Promise
<
AccessTokenStatus
>
{
const
response
=
await
api
.
get
<
ApiResponse
<
AccessTokenStatus
>>
(
'/api/user/token/status'
export
function
getAccessTokenStatus
():
Promise
<
AccessTokenStatus
>
{
return
authResult
(
api
.
get
(
'/api/user/token/status'
,
authRequestOptions
),
'Failed to load token status'
)
if
(
!
response
.
data
.
success
||
!
response
.
data
.
data
)
{
throw
new
Error
(
response
.
data
.
message
||
'Failed to load token status'
)
}
return
response
.
data
.
data
}
export
async
function
createAccessToken
():
Promise
<
string
>
{
const
response
=
await
api
.
post
<
ApiResponse
<
string
>>
(
'/api/user/token'
)
if
(
!
response
.
data
.
success
||
!
response
.
data
.
data
)
{
throw
new
Error
(
response
.
data
.
message
||
'Failed to generate token'
)
}
return
response
.
data
.
data
export
async
function
createAccessToken
(
proofToken
:
string
,
signal
:
AbortSignal
):
Promise
<
string
>
{
const
token
=
await
authResult
<
string
>
(
api
.
post
(
'/api/user/token'
,
undefined
,
{
...
authRequestOptions
,
headers
:
{
'X-Security-Proof'
:
proofToken
},
singleUseAuthorization
:
true
,
signal
,
}),
'Failed to generate token'
)
if
(
!
token
)
throw
new
AuthOperationError
(
'Failed to generate token'
)
return
token
}
export
async
function
revokeAccessToken
():
Promise
<
void
>
{
const
response
=
await
api
.
delete
<
ApiResponse
>
(
'/api/user/token'
)
if
(
!
response
.
data
.
success
)
{
throw
new
Error
(
response
.
data
.
message
||
'Failed to revoke token'
)
}
export
async
function
revokeAccessToken
(
proofToken
:
string
,
signal
:
AbortSignal
):
Promise
<
void
>
{
await
authResult
<
null
>
(
api
.
delete
(
'/api/user/token'
,
{
...
authRequestOptions
,
headers
:
{
'X-Security-Proof'
:
proofToken
},
singleUseAuthorization
:
true
,
signal
,
}),
'Failed to revoke token'
)
}
export
interface
TwoFASetupData
{
...
...
web/src/features/security/components/__tests__/access-token-card.test.tsx
View file @
a8729b5c
This diff is collapsed.
Click to expand it.
web/src/features/security/components/access-token-card.tsx
View file @
a8729b5c
...
...
@@ -30,6 +30,7 @@ import {
SheetHeader
,
SheetTitle
,
}
from
'@/components/ui/sheet'
import
{
SecureVerificationDialog
}
from
'@/features/auth/secure-verification'
import
{
AuditLogViewer
}
from
'@/features/usage-logs/audit/components/audit-log-viewer'
import
dayjs
from
'@/lib/dayjs'
...
...
@@ -43,21 +44,19 @@ export function AccessTokenCard() {
null
)
const
[
historyOpen
,
setHistoryOpen
]
=
useState
(
false
)
const
pending
=
access
.
generate
.
isPending
||
access
.
revoke
.
isP
ending
const
pending
=
access
.
p
ending
const
status
=
access
.
status
.
data
const
ready
=
!
access
.
status
.
isError
&&
!
access
.
status
.
isPending
&&
!!
status
let
lastUsed
=
t
(
'Unknown'
)
if
(
status
?.
last_used_at
)
{
lastUsed
=
dayjs
.
unix
(
status
.
last_used_at
).
format
(
'YYYY-MM-DD HH:mm:ss'
)
}
else
if
(
status
?.
created_at
)
lastUsed
=
t
(
'Not used yet'
)
const
confirm
=
async
()
=>
{
try
{
if
(
confirmation
===
'revoke'
)
await
access
.
revoke
.
mutateAsync
()
else
await
access
.
generate
.
mutateAsync
()
setConfirmation
(
null
)
}
catch
{
/* The mutation displays the error and preserves the confirmation. */
}
const
confirm
=
()
=>
{
if
(
pending
||
!
confirmation
)
return
const
operation
=
confirmation
setConfirmation
(
null
)
if
(
operation
===
'revoke'
)
void
access
.
revoke
()
else
void
access
.
generate
()
}
return
(
<>
...
...
@@ -152,7 +151,7 @@ export function AccessTokenCard() {
<
Button
size=
'sm'
disabled=
{
pending
}
onClick=
{
()
=>
access
.
generate
.
mut
ate
()
}
onClick=
{
()
=>
void
access
.
gener
ate
()
}
>
{
t
(
'Generate'
)
}
</
Button
>
...
...
@@ -164,6 +163,7 @@ export function AccessTokenCard() {
{
access
.
token
&&
(
<
AccessTokenDialog
token=
{
access
.
token
}
onClose=
{
access
.
clearToken
}
/>
)
}
<
SecureVerificationDialog
{
...
access
.
verificationDialogProps
}
/>
<
ConfirmDialog
open=
{
confirmation
!==
null
}
onOpenChange=
{
(
open
)
=>
{
...
...
web/src/features/security/hooks/use-access-token.ts
View file @
a8729b5c
...
...
@@ -16,11 +16,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import
{
use
Mutation
,
use
Query
,
useQueryClient
}
from
'@tanstack/react-query'
import
{
useState
}
from
'react'
import
{
useQuery
,
useQueryClient
}
from
'@tanstack/react-query'
import
{
use
Callback
,
useEffect
,
useRef
,
use
State
}
from
'react'
import
{
useTranslation
}
from
'react-i18next'
import
{
toast
}
from
'sonner'
import
{
useSecureVerification
}
from
'@/features/auth/secure-verification'
import
{
AuthOperationError
}
from
'@/lib/secure-verification'
import
{
useAuthStore
}
from
'@/stores/auth-store'
import
{
...
...
@@ -33,36 +35,93 @@ export function useAccessToken() {
const
{
t
}
=
useTranslation
()
const
client
=
useQueryClient
()
const
userId
=
useAuthStore
((
state
)
=>
state
.
auth
.
user
?.
id
)
const
sessionId
=
useAuthStore
((
state
)
=>
state
.
auth
.
session
?.
sid
)
const
statusKey
=
[
'security'
,
'access-token'
,
'status'
,
userId
]
as
const
const
[
token
,
setToken
]
=
useState
(
''
)
const
[
generatedToken
,
setGeneratedToken
]
=
useState
<
{
value
:
string
userId
:
number
|
undefined
sessionId
:
string
|
undefined
}
|
null
>
(
null
)
const
[
pending
,
setPending
]
=
useState
(
false
)
const
currentOperation
=
useRef
<
AbortController
|
null
>
(
null
)
const
verification
=
useSecureVerification
()
const
requestVerification
=
verification
.
requestVerification
const
cancelVerification
=
verification
.
cancel
const
status
=
useQuery
({
queryKey
:
statusKey
,
queryFn
:
getAccessTokenStatus
,
retry
:
false
,
})
const
refresh
=
()
=>
client
.
invalidateQueries
({
queryKey
:
statusKey
})
const
generate
=
useMutation
({
// Keep plaintext out of the query/mutation cache and persistent storage.
mutationFn
:
async
()
=>
{
setToken
(
await
createAccessToken
())
useEffect
(
()
=>
()
=>
{
const
current
=
currentOperation
.
current
currentOperation
.
current
=
null
current
?.
abort
()
cancelVerification
()
setPending
(
false
)
setGeneratedToken
(
null
)
},
onSuccess
:
refresh
,
onError
:
()
=>
{
toast
.
error
(
t
(
'Failed to generate token'
))
void
refresh
()
},
})
const
revoke
=
useMutation
({
mutationFn
:
revokeAccessToken
,
onSuccess
:
()
=>
{
setToken
(
''
)
toast
.
success
(
t
(
'Access token revoked'
))
return
refresh
()
[
cancelVerification
,
userId
,
sessionId
]
)
const
performOperation
=
useCallback
(
async
(
operation
:
'generate'
|
'revoke'
)
=>
{
if
(
currentOperation
.
current
)
return
const
controller
=
new
AbortController
()
currentOperation
.
current
=
controller
setPending
(
true
)
try
{
const
proof
=
await
requestVerification
({
scope
:
`access_token.
${
operation
}
`
,
})
if
(
currentOperation
.
current
!==
controller
||
!
proof
)
return
// Proofs and plaintext stay local to this action, outside React Query caches.
if
(
operation
===
'generate'
)
{
const
generated
=
await
createAccessToken
(
proof
.
proof_token
,
controller
.
signal
)
if
(
currentOperation
.
current
!==
controller
)
return
setGeneratedToken
({
value
:
generated
,
userId
,
sessionId
})
}
else
{
await
revokeAccessToken
(
proof
.
proof_token
,
controller
.
signal
)
if
(
currentOperation
.
current
!==
controller
)
return
setGeneratedToken
(
null
)
toast
.
success
(
t
(
'Access token revoked'
))
}
void
client
.
invalidateQueries
({
queryKey
:
[
'security'
,
'access-token'
,
'status'
,
userId
],
})
}
catch
(
error
)
{
if
(
currentOperation
.
current
!==
controller
)
return
const
failure
=
AuthOperationError
.
from
(
error
)
if
(
failure
.
code
!==
'AUTH_CANCELLED'
)
toast
.
error
(
t
(
failure
.
message
))
void
client
.
invalidateQueries
({
queryKey
:
[
'security'
,
'access-token'
,
'status'
,
userId
],
})
}
finally
{
if
(
currentOperation
.
current
===
controller
)
{
currentOperation
.
current
=
null
setPending
(
false
)
}
}
},
onError
:
()
=>
{
toast
.
error
(
t
(
'Failed to revoke token'
))
void
refresh
()
[
client
,
requestVerification
,
t
,
userId
,
sessionId
]
)
return
{
status
,
token
:
generatedToken
?.
userId
===
userId
&&
generatedToken
?.
sessionId
===
sessionId
?
(
generatedToken
?.
value
??
''
)
:
''
,
pending
,
clearToken
:
()
=>
{
setGeneratedToken
(
null
)
},
})
return
{
status
,
token
,
clearToken
:
()
=>
setToken
(
''
),
generate
,
revoke
}
generate
:
()
=>
performOperation
(
'generate'
),
revoke
:
()
=>
performOperation
(
'revoke'
),
verificationDialogProps
:
verification
.
dialogProps
,
}
}
web/src/lib/secure-verification.ts
View file @
a8729b5c
...
...
@@ -64,20 +64,19 @@ export const authRequestOptions = {
export
async
function
authResult
<
T
>
(
request
:
Promise
<
{
data
:
{
success
:
boolean
;
message
?:
string
;
code
?:
string
;
data
?:
T
}
}
>
}
>
,
fallback
=
'Verification failed. Please try again.'
):
Promise
<
T
>
{
try
{
const
{
data
:
response
}
=
await
request
if
(
!
response
.
success
||
response
.
data
===
undefined
)
{
throw
new
AuthOperationError
(
getServerErrorMessageKey
(
response
)
||
response
.
message
||
'Verification failed. Please try again.'
,
getServerErrorMessageKey
(
response
)
||
response
.
message
||
fallback
,
response
.
code
)
}
return
response
.
data
}
catch
(
error
)
{
throw
AuthOperationError
.
from
(
error
)
throw
AuthOperationError
.
from
(
error
,
fallback
)
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment