Skip to content

Commit

Permalink
refactor(sign-in): redirect to intended page after signing in
Browse files Browse the repository at this point in the history
Fixes goplus#985
Fixes goplus#986

Signed-off-by: Aofei Sheng <aofei@aofeisheng.com>
  • Loading branch information
aofei committed Oct 17, 2024
1 parent caa3fbe commit eb880d9
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 233 deletions.
22 changes: 21 additions & 1 deletion spx-gui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion spx-gui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@
"@vue/runtime-dom": "^3.4.10",
"@vue/test-utils": "^2.4.5",
"@vue/tsconfig": "^0.5.1",
"casdoor-js-sdk": "^0.15.0",
"dayjs": "^1.11.10",
"eslint": "^8.56.0",
"eslint-plugin-vue": "^9.20.1",
"file-saver": "^2.0.5",
"happy-dom": "^14.3.6",
"install": "^0.13.0",
"js-pkce": "^1.4.0",
"jszip": "^3.10.1",
"jwt-decode": "^4.0.0",
"konva": "^9.3.1",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
Expand Down
2 changes: 1 addition & 1 deletion spx-gui/src/components/navbar/NavbarProfile.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div v-if="!userStore.userInfo" class="sign-in">
<UIButton type="secondary" :disabled="!isOnline" @click="userStore.signInWithRedirection()">{{
<UIButton type="secondary" :disabled="!isOnline" @click="userStore.initiateSignIn()">{{
$t({ en: 'Sign in', zh: '登录' })
}}</UIButton>
</div>
Expand Down
2 changes: 1 addition & 1 deletion spx-gui/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ dayjs.extend(relativeTime)

const initApiClient = async () => {
const userStore = useUserStore()
client.setAuthProvider(userStore.getFreshAccessToken)
client.setAuthProvider(userStore.ensureAccessToken)
}

async function initApp() {
Expand Down
2 changes: 1 addition & 1 deletion spx-gui/src/pages/editor/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ watchEffect(() => {
// This will be called on mount and whenever userStore changes,
// which are the cases when userStore.signOut() is called
if (!userStore.isSignedIn) {
userStore.signInWithRedirection()
userStore.initiateSignIn()
}
})
Expand Down
9 changes: 7 additions & 2 deletions spx-gui/src/pages/sign-in/callback.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@ try {
if (lang === 'en' || lang === 'zh') {
i18n.setLang(lang)
}
await userStore.consumeCurrentUrl()
} finally {
await userStore.completeSignIn()
const returnTo = params.get('returnTo')
window.location.replace(returnTo != null ? returnTo : '/')
} catch (e) {
console.error('failed to complete sign-in', e)
window.location.replace('/')
}
</script>
Expand Down
147 changes: 71 additions & 76 deletions spx-gui/src/stores/user.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,30 @@
import CasdoorSdk from '@/utils/casdoor'
import Sdk from 'casdoor-js-sdk'
import { casdoorConfig } from '@/utils/env'
import type ITokenResponse from 'js-pkce/dist/ITokenResponse'
import { jwtDecode } from 'jwt-decode'
import { defineStore } from 'pinia'

// https://stackoverflow.com/questions/38552003/how-to-decode-jwt-token-in-javascript-without-using-a-library
const parseJwt = (token: string) => {
const base64Url = token.split('.')[1]
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
})
.join('')
)

return JSON.parse(jsonPayload)
}

export interface UserInfo {
name: string
id: string
name: string
displayName: string
avatar: string
email: string
emailVerified: boolean
phone: string
}

const casdoorSdk = new CasdoorSdk({
interface TokenResponse {
access_token: string
refresh_token: string
expires_in: number
refresh_expires_in: number
}

const casdoorAuthRedirectPath = '/callback'
const casdoorSdk = new Sdk({
...casdoorConfig,
redirectPath: '/callback'
redirectPath: casdoorAuthRedirectPath
})

const tokenExpiryDelta = 60 * 1000 // 1 minute in milliseconds

export const useUserStore = defineStore('spx-user', {
state: () => ({
accessToken: null as string | null,
Expand All @@ -43,74 +34,78 @@ export const useUserStore = defineStore('spx-user', {
accessTokenExpiresAt: null as number | null,
refreshTokenExpiresAt: null as number | null
}),
getters: {
isAccessTokenValid(): boolean {
return !!(
this.accessToken &&
(this.accessTokenExpiresAt === null ||
this.accessTokenExpiresAt - tokenExpiryDelta > Date.now())
)
},
isRefreshTokenValid(): boolean {
return !!(
this.refreshToken &&
(this.refreshTokenExpiresAt === null ||
this.refreshTokenExpiresAt - tokenExpiryDelta > Date.now())
)
},
isSignedIn(): boolean {
return this.isAccessTokenValid || this.isRefreshTokenValid
},
userInfo(): UserInfo | null {
if (!this.isSignedIn) return null
return jwtDecode<UserInfo>(this.accessToken!)
}
},
actions: {
async getFreshAccessToken(): Promise<string | null> {
initiateSignIn(
returnTo: string = window.location.pathname + window.location.search + window.location.hash
) {
// Workaround for casdoor-js-sdk not supporting override of `redirectPath` in `signin_redirect`.
const casdoorSdk = new Sdk({
...casdoorConfig,
redirectPath: `${casdoorAuthRedirectPath}?returnTo=${encodeURIComponent(returnTo)}`
})
casdoorSdk.signin_redirect()
},
async completeSignIn() {
const resp = await casdoorSdk.exchangeForAccessToken()
this.handleTokenResponse(resp)
},
signOut() {
this.accessToken = null
this.refreshToken = null
this.accessTokenExpiresAt = null
this.refreshTokenExpiresAt = null
},
async ensureAccessToken(): Promise<string | null> {
if (this.isAccessTokenValid) return this.accessToken

if (this.isRefreshTokenValid) {
try {
const tokenResp = await casdoorSdk.pkce.refreshAccessToken(this.refreshToken!)
this.setToken(tokenResp)
const resp = await casdoorSdk.refreshAccessToken(this.refreshToken!)
this.handleTokenResponse(resp)
} catch (e) {
console.error('Failed to refresh access token', e)
console.error('failed to refresh access token', e)
throw e
}

// Due to js-pkce's lack of error handling, we must check if the access token is valid after calling `PKCE.refreshAccessToken`.
// The token might still be invalid if, e.g., the server has already revoked the refresh token.
// Due to casdoor-js-sdk's lack of error handling, we must check if the access token is valid after calling
// `casdoorSdk.refreshAccessToken`. The token might still be invalid if, e.g., the server has already revoked
// the refresh token.
if (this.isAccessTokenValid) return this.accessToken
}

this.signOut()
return null
},
setToken(tokenResp: ITokenResponse) {
const accessTokenExpiresAt = tokenResp.expires_in
? Date.now() + tokenResp.expires_in * 1000
handleTokenResponse(resp: TokenResponse) {
this.accessToken = resp.access_token
this.refreshToken = resp.refresh_token
this.accessTokenExpiresAt = resp.expires_in ? Date.now() + resp.expires_in * 1000 : null
this.refreshTokenExpiresAt = resp.refresh_expires_in
? Date.now() + resp.refresh_expires_in * 1000
: null
const refreshTokenExpiresAt = tokenResp.refresh_expires_in
? Date.now() + tokenResp.refresh_expires_in * 1000
: null
this.accessToken = tokenResp.access_token
this.refreshToken = tokenResp.refresh_token
this.accessTokenExpiresAt = accessTokenExpiresAt
this.refreshTokenExpiresAt = refreshTokenExpiresAt
},
signOut() {
this.accessToken = null
this.refreshToken = null
this.accessTokenExpiresAt = null
this.refreshTokenExpiresAt = null
},
async consumeCurrentUrl() {
const tokenResp = await casdoorSdk.pkce.exchangeForAccessToken(window.location.href)
this.setToken(tokenResp)
},
signInWithRedirection() {
casdoorSdk.signinWithRedirection()
}
},
getters: {
isAccessTokenValid(state): boolean {
const delta = 60 * 1000 // 1 minute
return !!(
state.accessToken &&
(state.accessTokenExpiresAt === null || state.accessTokenExpiresAt - delta > Date.now())
)
},
isRefreshTokenValid(state): boolean {
const delta = 60 * 1000 // 1 minute
return !!(
state.refreshToken &&
(state.refreshTokenExpiresAt === null || state.refreshTokenExpiresAt - delta > Date.now())
)
},
isSignedIn(): boolean {
return this.isAccessTokenValid || this.isRefreshTokenValid
},
userInfo(state): UserInfo | null {
if (!this.isSignedIn) return null
return parseJwt(state.accessToken!) as UserInfo
}
},
persist: true
Expand Down
Loading

0 comments on commit eb880d9

Please sign in to comment.