Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
W
wiki-js
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
1
Issues
1
List
Board
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jacklull
wiki-js
Commits
eed36755
Unverified
Commit
eed36755
authored
May 30, 2023
by
NGPixel
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: graphql auto refresh token + apollo improvements
parent
e9fc514c
Show whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
175 additions
and
33 deletions
+175
-33
auth.mjs
server/core/auth.mjs
+2
-2
servers.mjs
server/core/servers.mjs
+4
-3
authentication.mjs
server/graph/resolvers/authentication.mjs
+34
-0
authentication.graphql
server/graph/schemas/authentication.graphql
+9
-0
users.mjs
server/models/users.mjs
+2
-3
App.vue
ux/src/App.vue
+5
-3
apollo.js
ux/src/boot/apollo.js
+60
-7
AuthLoginPanel.vue
ux/src/components/AuthLoginPanel.vue
+1
-1
AdminGeneral.vue
ux/src/pages/AdminGeneral.vue
+6
-0
AdminLogin.vue
ux/src/pages/AdminLogin.vue
+3
-0
ProfileAvatar.vue
ux/src/pages/ProfileAvatar.vue
+3
-0
user.js
ux/src/stores/user.js
+46
-14
No files found.
server/core/auth.mjs
View file @
eed36755
...
...
@@ -109,7 +109,7 @@ export default {
* @param {Express Next Callback} next
*/
authenticate
(
req
,
res
,
next
)
{
WIKI
.
auth
.
passport
.
authenticate
(
'jwt'
,
{
session
:
false
},
async
(
err
,
user
,
info
)
=>
{
WIKI
.
auth
.
passport
.
authenticate
(
'jwt'
,
{
session
:
false
},
async
(
err
,
user
,
info
)
=>
{
if
(
err
)
{
return
next
()
}
let
mustRevalidate
=
false
const
strategyId
=
user
.
pvd
...
...
@@ -141,7 +141,7 @@ export default {
}
// Revalidate and renew token
if
(
mustRevalidate
)
{
if
(
mustRevalidate
&&
!
req
.
path
.
startsWith
(
'/_graphql'
)
)
{
const
jwtPayload
=
jwt
.
decode
(
extractJWT
(
req
))
try
{
const
newToken
=
await
WIKI
.
db
.
users
.
refreshToken
(
jwtPayload
.
id
,
jwtPayload
.
pvd
)
...
...
server/core/servers.mjs
View file @
eed36755
...
...
@@ -133,17 +133,18 @@ export default {
const
graphqlSchema
=
await
initSchema
()
this
.
graph
=
new
ApolloServer
({
schema
:
graphqlSchema
,
allowBatchedHttpRequests
:
true
,
csrfPrevention
:
true
,
cache
:
'bounded'
,
plugins
:
[
process
.
env
.
NODE_ENV
===
'development'
?
ApolloServerPluginLandingPageLocalDefault
({
process
.
env
.
NODE_ENV
===
'production'
?
ApolloServerPluginLandingPageProductionDefault
({
footer
:
false
})
:
ApolloServerPluginLandingPageLocalDefault
({
footer
:
false
,
embed
:
{
endpointIsEditable
:
false
,
runTelemetry
:
false
}
})
:
ApolloServerPluginLandingPageProductionDefault
({
footer
:
false
})
// ApolloServerPluginDrainHttpServer({ httpServer: this.http })
// ...(this.https && ApolloServerPluginDrainHttpServer({ httpServer: this.https }))
...
...
server/graph/resolvers/authentication.mjs
View file @
eed36755
import
_
from
'lodash-es'
import
{
generateError
,
generateSuccess
}
from
'../../helpers/graph.mjs'
import
jwt
from
'jsonwebtoken'
import
ms
from
'ms'
import
{
DateTime
}
from
'luxon'
export
default
{
Query
:
{
...
...
@@ -149,6 +152,37 @@ export default {
}
},
/**
* Refresh Token
*/
async
refreshToken
(
obj
,
args
,
context
)
{
try
{
let
decoded
=
{}
if
(
!
args
.
token
)
{
throw
new
Error
(
'ERR_MISSING_TOKEN'
)
}
try
{
decoded
=
jwt
.
verify
(
args
.
token
,
WIKI
.
config
.
auth
.
certs
.
public
,
{
audience
:
WIKI
.
config
.
auth
.
audience
,
issuer
:
'urn:wiki.js'
,
algorithms
:
[
'RS256'
],
ignoreExpiration
:
true
})
}
catch
(
err
)
{
throw
new
Error
(
'ERR_INVALID_TOKEN'
)
}
if
(
DateTime
.
utc
().
minus
(
ms
(
WIKI
.
config
.
auth
.
tokenRenewal
))
>
DateTime
.
fromSeconds
(
decoded
.
exp
))
{
throw
new
Error
(
'ERR_EXPIRED_TOKEN'
)
}
const
newToken
=
await
WIKI
.
db
.
users
.
refreshToken
(
decoded
.
id
)
return
{
jwt
:
newToken
.
token
,
operation
:
generateSuccess
(
'Token refreshed successfully'
)
}
}
catch
(
err
)
{
return
generateError
(
err
)
}
},
/**
* Set API state
*/
async
setApiState
(
obj
,
args
,
context
)
{
...
...
server/graph/schemas/authentication.graphql
View file @
eed36755
...
...
@@ -58,6 +58,10 @@ extend type Mutation {
name
:
String
!
):
AuthenticationRegisterResponse
refreshToken
(
token
:
String
!
):
AuthenticationTokenResponse
@
rateLimit
(
limit
:
30
,
duration
:
60)
revokeApiKey
(
id
:
UUID
!
):
DefaultResponse
...
...
@@ -128,6 +132,11 @@ type AuthenticationRegisterResponse {
jwt
:
String
}
type
AuthenticationTokenResponse
{
operation
:
Operation
jwt
:
String
}
input
AuthenticationStrategyInput
{
key
:
String
!
strategyKey
:
String
!
...
...
server/models/users.mjs
View file @
eed36755
...
...
@@ -386,7 +386,7 @@ export class User extends Model {
/**
* Generate a new token for a user
*/
static
async
refreshToken
(
user
,
provid
er
)
{
static
async
refreshToken
(
us
er
)
{
if
(
isString
(
user
))
{
user
=
await
WIKI
.
db
.
users
.
query
().
findById
(
user
).
withGraphFetched
(
'groups'
).
modifyGraph
(
'groups'
,
builder
=>
{
builder
.
select
(
'groups.id'
,
'permissions'
)
...
...
@@ -411,8 +411,7 @@ export class User extends Model {
token
:
jwt
.
sign
({
id
:
user
.
id
,
email
:
user
.
email
,
groups
:
user
.
getGroups
(),
...
provider
&&
{
pvd
:
provider
}
groups
:
user
.
getGroups
()
},
{
key
:
WIKI
.
config
.
auth
.
certs
.
private
,
passphrase
:
WIKI
.
config
.
auth
.
secret
...
...
ux/src/App.vue
View file @
eed36755
...
...
@@ -125,6 +125,11 @@ if (typeof siteConfig !== 'undefined') {
router
.
beforeEach
(
async
(
to
,
from
)
=>
{
commonStore
.
routerLoading
=
true
// -> Init Auth Token
if
(
userStore
.
token
&&
!
userStore
.
authenticated
)
{
userStore
.
loadToken
()
}
// -> System Flags
if
(
!
flagsStore
.
loaded
)
{
flagsStore
.
load
()
...
...
@@ -144,9 +149,6 @@ router.beforeEach(async (to, from) => {
applyLocale
(
commonStore
.
desiredLocale
)
}
// -> User Auth
await
userStore
.
refreshAuth
()
// -> User Profile
if
(
userStore
.
authenticated
&&
!
userStore
.
profileLoaded
)
{
console
.
info
(
`Refreshing user
${
userStore
.
id
}
profile...`
)
...
...
ux/src/boot/apollo.js
View file @
eed36755
import
{
boot
}
from
'quasar/wrappers'
import
{
ApolloClient
,
InMemoryCache
}
from
'@apollo/client/core'
import
{
ApolloClient
,
HttpLink
,
InMemoryCache
,
from
,
split
}
from
'@apollo/client/core'
import
{
setContext
}
from
'@apollo/client/link/context'
import
{
BatchHttpLink
}
from
'@apollo/client/link/batch-http'
import
{
createUploadLink
}
from
'apollo-upload-client'
import
{
useUserStore
}
from
'src/stores/user'
...
...
@@ -8,27 +9,80 @@ import { useUserStore } from 'src/stores/user'
export
default
boot
(({
app
})
=>
{
const
userStore
=
useUserStore
()
const
defaultLinkOptions
=
{
uri
:
'/_graphql'
,
credentials
:
'omit'
}
let
refreshPromise
=
null
let
fetching
=
false
// Authentication Link
const
authLink
=
setContext
(
async
(
req
,
{
headers
})
=>
{
const
token
=
userStore
.
token
if
(
!
userStore
.
token
)
{
return
{
headers
:
{
...
headers
,
Authorization
:
''
}
}
}
// -> Refresh Token
if
(
!
userStore
.
isTokenValid
())
{
if
(
!
fetching
)
{
refreshPromise
=
new
Promise
((
resolve
,
reject
)
=>
{
(
async
()
=>
{
fetching
=
true
try
{
await
userStore
.
refreshToken
()
resolve
()
}
catch
(
err
)
{
reject
(
err
)
}
fetching
=
false
})()
})
}
else
{
// -> Another request is already executing, wait for it to complete
await
refreshPromise
}
}
return
{
headers
:
{
...
headers
,
Authorization
:
token
?
`Bearer
${
token
}
`
:
''
Authorization
:
userStore
.
token
?
`Bearer
${
userStore
.
token
}
`
:
''
}
}
})
// Upload / HTTP Link
const
uploadLink
=
createUploadLink
({
uri
()
{
return
'/_graphql'
...
defaultLinkOptions
,
headers
:
{
'Apollo-Require-Preflight'
:
'true'
}
})
// Directional Link
const
link
=
split
(
op
=>
op
.
getContext
().
skipAuth
,
new
HttpLink
(
defaultLinkOptions
),
from
([
authLink
,
split
(
op
=>
op
.
getContext
().
uploadMode
,
uploadLink
,
new
BatchHttpLink
(
defaultLinkOptions
)
)
])
)
// Cache
const
cache
=
new
InMemoryCache
()
// Restore SSR state
if
(
typeof
window
!==
'undefined'
)
{
const
state
=
window
.
__APOLLO_STATE__
if
(
state
)
{
...
...
@@ -39,8 +93,7 @@ export default boot(({ app }) => {
// Client
const
client
=
new
ApolloClient
({
cache
,
link
:
authLink
.
concat
(
uploadLink
),
credentials
:
'omit'
,
link
,
ssrForceFetchDelay
:
100
})
...
...
ux/src/components/AuthLoginPanel.vue
View file @
eed36755
...
...
@@ -502,7 +502,7 @@ async function handleLoginResponse (resp) {
$q
.
loading
.
show
({
message
:
t
(
'auth.loginSuccess'
)
})
Cookies
.
set
(
'jwt'
,
resp
.
jwt
,
{
expires
:
365
})
Cookies
.
set
(
'jwt'
,
resp
.
jwt
,
{
expires
:
365
,
path
:
'/'
,
sameSite
:
'Lax'
})
setTimeout
(()
=>
{
const
loginRedirect
=
Cookies
.
get
(
'loginRedirect'
)
if
(
loginRedirect
===
'/'
&&
resp
.
redirect
)
{
...
...
ux/src/pages/AdminGeneral.vue
View file @
eed36755
...
...
@@ -745,6 +745,9 @@ async function uploadLogo () {
state
.
loading
++
try
{
const
resp
=
await
APOLLO_CLIENT
.
mutate
({
context
:
{
uploadMode
:
true
},
mutation
:
gql
`
mutation uploadLogo (
$id: UUID!
...
...
@@ -796,6 +799,9 @@ async function uploadFavicon () {
state
.
loading
++
try
{
const
resp
=
await
APOLLO_CLIENT
.
mutate
({
context
:
{
uploadMode
:
true
},
mutation
:
gql
`
mutation uploadFavicon (
$id: UUID!
...
...
ux/src/pages/AdminLogin.vue
View file @
eed36755
...
...
@@ -308,6 +308,9 @@ async function uploadBg () {
state
.
loading
++
try
{
const
resp
=
await
APOLLO_CLIENT
.
mutate
({
context
:
{
uploadMode
:
true
},
mutation
:
gql
`
mutation uploadLoginBg (
$id: UUID!
...
...
ux/src/pages/ProfileAvatar.vue
View file @
eed36755
...
...
@@ -97,6 +97,9 @@ async function uploadImage () {
state
.
loading
++
try
{
const
resp
=
await
APOLLO_CLIENT
.
mutate
({
context
:
{
uploadMode
:
true
},
mutation
:
gql
`
mutation uploadUserAvatar (
$id: UUID!
...
...
ux/src/stores/user.js
View file @
eed36755
...
...
@@ -24,7 +24,7 @@ export const useUserStore = defineStore('user', {
iat
:
0
,
exp
:
null
,
authenticated
:
false
,
token
:
''
,
token
:
Cookies
.
get
(
'jwt'
)
,
profileLoaded
:
false
}),
getters
:
{
...
...
@@ -40,28 +40,60 @@ export const useUserStore = defineStore('user', {
}
},
actions
:
{
async
refreshAuth
()
{
if
(
this
.
exp
&&
this
.
exp
<
DateTime
.
now
())
{
return
}
const
jwtCookie
=
Cookies
.
get
(
'jwt'
)
if
(
jwtCookie
)
{
isTokenValid
()
{
return
this
.
exp
&&
this
.
exp
>
DateTime
.
now
()
},
loadToken
()
{
if
(
!
this
.
token
)
{
return
}
try
{
const
jwtData
=
jwtDecode
(
jwtCookie
)
const
jwtData
=
jwtDecode
(
this
.
token
)
this
.
id
=
jwtData
.
id
this
.
email
=
jwtData
.
email
this
.
iat
=
jwtData
.
iat
this
.
exp
=
DateTime
.
fromSeconds
(
jwtData
.
exp
,
{
zone
:
'utc'
})
this
.
token
=
jwtCookie
if
(
this
.
exp
<=
DateTime
.
utc
())
{
console
.
info
(
'Token has expired. Attempting renew...'
)
// TODO: Renew token
}
else
{
if
(
this
.
exp
>
DateTime
.
utc
())
{
this
.
authenticated
=
true
}
else
{
console
.
info
(
'Token has expired and will be refreshed on next query.'
)
}
}
catch
(
err
)
{
console
.
debug
(
'Invalid JWT. Silent authentication skipped.'
)
console
.
warn
(
'Failed to parse JWT. Invalid or malformed.'
)
}
},
async
refreshToken
()
{
try
{
const
respRaw
=
await
APOLLO_CLIENT
.
mutate
({
context
:
{
skipAuth
:
true
},
mutation
:
gql
`
mutation refreshToken (
$token: String!
) {
refreshToken(token: $token) {
operation {
succeeded
message
}
jwt
}
}
`
,
variables
:
{
token
:
this
.
token
}
})
const
resp
=
respRaw
?.
data
?.
refreshToken
??
{}
if
(
!
resp
.
operation
?.
succeeded
)
{
throw
new
Error
(
resp
.
operation
?.
message
||
'Failed to refresh token.'
)
}
Cookies
.
set
(
'jwt'
,
resp
.
jwt
,
{
expires
:
365
,
path
:
'/'
,
sameSite
:
'Lax'
})
this
.
token
=
resp
.
jwt
this
.
loadToken
()
return
true
}
catch
(
err
)
{
console
.
warn
(
err
)
return
false
}
},
async
refreshProfile
()
{
...
...
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