Skip to content

Commit

Permalink
feat(auth): introduce sign-in guard
Browse files Browse the repository at this point in the history
- Add `withSignInGuard` HOF to prompt users to sign in before executing
  protected actions.
- Add similar protection for specific routes using navigation guards.

Signed-off-by: Aofei Sheng <aofei@aofeisheng.com>
  • Loading branch information
aofei committed Oct 18, 2024
1 parent 0050905 commit bed12d7
Show file tree
Hide file tree
Showing 10 changed files with 65 additions and 33 deletions.
6 changes: 3 additions & 3 deletions spx-gui/src/components/community/user/FollowButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { computed, ref, watch } from 'vue'
import { useUserStore } from '@/stores'
import { follow, isFollowing, unfollow } from '@/apis/user'
import { UIButton } from '@/components/ui'
import { useMessageHandle } from '@/utils/exception'
import { useMessageHandle, withSignInGuard } from '@/utils/exception'
const props = defineProps<{
/** Name of user to follow */
Expand All @@ -24,10 +24,10 @@ watch(
)
const handleClick = useMessageHandle(
async () => {
withSignInGuard(async () => {
await (following.value ? unfollow(props.name) : follow(props.name))
following.value = !following.value
},
}),
{ en: 'Failed to operate', zh: '操作失败' }
)
</script>
Expand Down
6 changes: 3 additions & 3 deletions spx-gui/src/components/navbar/NavbarNewProjectItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@
import { useRouter } from 'vue-router'
import { getProjectEditorRoute } from '@/router'
import { UIMenuItem } from '@/components/ui'
import { useMessageHandle } from '@/utils/exception'
import { useMessageHandle, withSignInGuard } from '@/utils/exception'
import { useCreateProject } from '@/components/project'
import newSvg from './icons/new.svg'
const router = useRouter()
const createProject = useCreateProject()
const handleNewProject = useMessageHandle(
async () => {
withSignInGuard(async () => {
const { name } = await createProject()
router.push(getProjectEditorRoute(name))
},
}),
{ en: 'Failed to create new project', zh: '新建项目失败' }
).fn
</script>
4 changes: 2 additions & 2 deletions spx-gui/src/components/navbar/NavbarOpenProjectItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@

<script setup lang="ts">
import { UIMenuItem } from '@/components/ui'
import { useMessageHandle } from '@/utils/exception'
import { useMessageHandle, withSignInGuard } from '@/utils/exception'
import { useOpenProject } from '@/components/project'
import openSvg from './icons/open.svg'
const openProject = useOpenProject()
const handleOpenProject = useMessageHandle(openProject, {
const handleOpenProject = useMessageHandle(withSignInGuard(openProject), {
en: 'Failed to open project',
zh: '打开项目失败'
}).fn
Expand Down
9 changes: 6 additions & 3 deletions spx-gui/src/components/navbar/NavbarProfile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@
</UIMenuItem>
</UIMenuGroup>
<UIMenuGroup>
<UIMenuItem @click="userStore.signOut()">{{
$t({ en: 'Sign out', zh: '登出' })
}}</UIMenuItem>
<UIMenuItem @click="handleSignOut">{{ $t({ en: 'Sign out', zh: '登出' }) }}</UIMenuItem>
</UIMenuGroup>
</UIMenu>
</UIDropdown>
Expand All @@ -51,6 +49,11 @@ function handleUserPage() {
function handleProjects() {
router.push(getUserPageRoute(userStore.userInfo!.name, 'projects'))
}
function handleSignOut() {
userStore.signOut()
router.go(0) // Reload the page to trigger navigation guards.
}
</script>

<style lang="scss" scoped>
Expand Down
19 changes: 13 additions & 6 deletions spx-gui/src/pages/community/explore.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
</template>

<script setup lang="ts">
import { useQuery } from '@/utils/exception'
import { useQuery, withSignInGuard } from '@/utils/exception'
import { useRouteQueryParamStrEnum } from '@/utils/route'
import { exploreProjects, ExploreOrder as Order } from '@/apis/project'
import { UITagRadioGroup, UITagRadio } from '@/components/ui'
Expand All @@ -38,13 +38,20 @@ const order = useRouteQueryParamStrEnum('o', Order, Order.MostLikes)
const maxCount = 50
const fetchProjects = () => {
return exploreProjects({
order: order.value,
count: maxCount
})
}
const fetchProjectsWithSignInGuard = withSignInGuard(fetchProjects)
const queryRet = useQuery(
() => {
// TODO: login prompt for unauthenticated users
return exploreProjects({
order: order.value,
count: maxCount
})
if (order.value === Order.FollowingCreated) {
return fetchProjectsWithSignInGuard()
}
return fetchProjects()
},
{
en: 'Failed to list projects',
Expand Down
6 changes: 3 additions & 3 deletions spx-gui/src/pages/community/user/projects.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useRouteQueryParamInt, useRouteQueryParamStrEnum } from '@/utils/route'
import { useMessageHandle, useQuery } from '@/utils/exception'
import { useMessageHandle, useQuery, withSignInGuard } from '@/utils/exception'
import { Visibility, listProject, type ListProjectParams } from '@/apis/project'
import { getProjectEditorRoute } from '@/router'
import { useUserStore } from '@/stores'
Expand Down Expand Up @@ -59,10 +59,10 @@ const queryRet = useQuery(() => listProject(listParams.value), {
const router = useRouter()
const createProject = useCreateProject()
const handleNewProject = useMessageHandle(
async () => {
withSignInGuard(async () => {
const { name } = await createProject()
router.push(getProjectEditorRoute(name))
},
}),
{ en: 'Failed to create new project', zh: '新建项目失败' }
).fn
</script>
Expand Down
7 changes: 0 additions & 7 deletions spx-gui/src/pages/editor/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,6 @@ const LOCAL_CACHE_KEY = 'GOPLUS_BUILDER_CACHED_PROJECT'
// TODO: move this to some outer position
const userStore = useUserStore()
watchEffect(() => {
// This will be called on mount and whenever userStore changes,
// which are the cases when userStore.signOut() is called
if (!userStore.isSignedIn) {
userStore.initiateSignIn()
}
})
const router = useRouter()
Expand Down
14 changes: 10 additions & 4 deletions spx-gui/src/router.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { App } from 'vue'
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
import type { ExploreOrder } from './apis/project'
import { useUserStore } from './stores'

export function getProjectEditorRoute(projectName: string) {
return `/editor/${projectName}`
Expand Down Expand Up @@ -94,12 +95,9 @@ const routes: Array<RouteRecordRaw> = [
{
path: '/editor/:projectName',
component: () => import('@/pages/editor/index.vue'),
meta: { requiresSignIn: true },
props: true
},
{
path: '/callback', // TODO: remove me
redirect: '/sign-in/callback'
},
{
path: '/sign-in/callback',
component: () => import('@/pages/sign-in/callback.vue')
Expand All @@ -120,6 +118,14 @@ const router = createRouter({
})

export const initRouter = async (app: App) => {
const userStore = useUserStore()
router.beforeEach((to, _, next) => {
if (to.meta.requiresSignIn && !userStore.isSignedIn) {
userStore.initiateSignIn()
} else {
next()
}
})
app.use(router)
// This is an example of a routing result that needs to be loaded.
await new Promise((resolve) => {
Expand Down
2 changes: 1 addition & 1 deletion spx-gui/src/stores/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface TokenResponse {
refresh_expires_in: number
}

const casdoorAuthRedirectPath = '/callback'
const casdoorAuthRedirectPath = '/sign-in/callback'
const casdoorSdk = new Sdk({
...casdoorConfig,
redirectPath: casdoorAuthRedirectPath
Expand Down
25 changes: 24 additions & 1 deletion spx-gui/src/utils/exception.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
* @desc Definition for Exceptions & tools to help handle them
*/

import { useMessage } from '@/components/ui'
import { useConfirmDialog, useMessage } from '@/components/ui'
import { useI18n } from './i18n'
import type { LocaleMessage } from './i18n'
import { ref, shallowRef, watchEffect, type Ref, type ShallowRef } from 'vue'
import { useUserStore } from '@/stores'

/**
* Exceptions are like errors, while slightly different:
Expand Down Expand Up @@ -134,6 +135,28 @@ export function useMessageHandle<Args extends any[], T>(
}
}

export function withSignInGuard<Args extends any[], T>(fn: (...args: Args) => Promise<T>) {
const { t } = useI18n()
const userStore = useUserStore()
const withConfirm = useConfirmDialog()

return async (...args: Args): Promise<T> => {
if (!userStore.isSignedIn) {
await withConfirm({
title: t({ en: 'Sign in to continue', zh: '登录以继续' }),
content: t({
en: 'You need to sign in first to perform this action. Would you like to sign in now?',
zh: '你需要先登录才能执行此操作。你想现在登录吗?'
}),
confirmText: t({ en: 'Sign in', zh: '登录' }),
confirmHandler: () => userStore.initiateSignIn()
})
throw new Cancelled('redirected to sign in')
}
return fn(...args)
}
}

export type QueryRet<T> = {
isLoading: Ref<boolean>
data: ShallowRef<T | null>
Expand Down

0 comments on commit bed12d7

Please sign in to comment.