554 copilot analyst user (#558)
* feat: implement access denied page and update error handling for permissions * refactor: clean up formatting and improve function declarations in AccessDenied component
taylor_socfortress committed
Dec 11, 2025 at 08:43 UTC
bf1cc35f1c0782582be72caf433dcd6ab57aa007
4 files changed
+119
-40
backend/app/auth/utils.py
+4
-4
@@ -233,8 +233,8 @@ class AuthHandler:
233
for scope in security_scopes.scopes:
234
if scope not in token_scopes:
235
raise HTTPException(
236
- status_code=401,
237
- detail="Not enough permissions",
236
+ status_code=403,
237
+ detail=f"Insufficient permissions. Required scope: {scope}",
238
headers={"WWW-Authenticate": authenticate_value},
239
)
240
@@ -289,8 +289,8 @@ class AuthHandler:
289
290
if not any(scope in token_scopes for scope in required_scopes):
291
raise HTTPException(
292
- status_code=401,
293
- detail="Not enough permissions, you don't have any of the required scopes.",
292
+ status_code=403,
293
+ detail=f"Insufficient permissions. Required one of: {', '.join(required_scopes)}",
294
headers={"WWW-Authenticate": "Bearer"},
295
)
296
customer_portal/src/api/httpClient.ts
+47
-36
@@ -4,51 +4,62 @@ import { useAuthStore } from "@/stores/auth"
4
import { isDebounceTimeOver, isJwtExpiring } from "@/utils/auth"
5
6
const HttpClient = axios.create({
7
- baseURL: "/api"
7
+ baseURL: "/api"
8
})
9
10
let __TOKEN_REFRESHING = false
11
let __TOKEN_LAST_CHECK: Date | null = null
12
13
HttpClient.interceptors.request.use(
14
- config => {
15
- const store = useAuthStore()
16
-
17
- if (!config.headers) config.headers = {} as AxiosRequestHeaders
18
- if (store.userToken) {
19
- config.headers.Authorization = `Bearer ${store.userToken}`
20
- }
21
-
22
- if (
23
- store.userToken &&
24
- isJwtExpiring(store.userToken, 60 * 60) &&
25
- !__TOKEN_REFRESHING &&
26
- isDebounceTimeOver(__TOKEN_LAST_CHECK)
27
- ) {
28
- __TOKEN_REFRESHING = true
29
- __TOKEN_LAST_CHECK = new Date()
30
-
31
- store.refreshToken().then(() => {
32
- __TOKEN_REFRESHING = false
33
- })
34
- }
35
-
36
- return config
37
- },
38
- error => Promise.reject(error)
14
+ config => {
15
+ const store = useAuthStore()
16
+
17
+ if (!config.headers) config.headers = {} as AxiosRequestHeaders
18
+ if (store.userToken) {
19
+ config.headers.Authorization = `Bearer ${store.userToken}`
20
+ }
21
+
22
+ if (
23
+ store.userToken &&
24
+ isJwtExpiring(store.userToken, 60 * 60) &&
25
+ !__TOKEN_REFRESHING &&
26
+ isDebounceTimeOver(__TOKEN_LAST_CHECK)
27
+ ) {
28
+ __TOKEN_REFRESHING = true
29
+ __TOKEN_LAST_CHECK = new Date()
30
+
31
+ store.refreshToken().then(() => {
32
+ __TOKEN_REFRESHING = false
33
+ })
34
+ }
35
+
36
+ return config
37
+ },
38
+ error => Promise.reject(error)
39
)
40
41
HttpClient.interceptors.response.use(
42
- response => response,
43
- error => {
44
- if (error.response && error.response.status === 401) {
45
- if (!window.location.pathname.includes("login")) {
46
- window.location.href = "/logout"
47
- }
48
- }
49
-
50
- return Promise.reject(error)
51
- }
42
+ response => response,
43
+ error => {
44
+ if (error.response) {
45
+ const status = error.response.status
46
+
47
+ if (status === 401) {
48
+ // Unauthorized - authentication failed (invalid/expired token)
49
+ if (!window.location.pathname.includes("login")) {
50
+ window.location.href = "/logout"
51
+ }
52
+ } else if (status === 403) {
53
+ // Forbidden - insufficient permissions (keep user logged in)
54
+ if (!window.location.pathname.includes("access-denied")) {
55
+ const message = error.response.data?.detail || "You do not have permission to access this resource."
56
+ window.location.href = `/access-denied?message=${encodeURIComponent(message)}`
57
+ }
58
+ }
59
+ }
60
+
61
+ return Promise.reject(error)
62
+ }
63
)
64
65
export { HttpClient }
customer_portal/src/router/index.ts
+7
@@ -5,6 +5,7 @@ import AlertsPage from "@/views/AlertsPage.vue"
5
import CasesPage from "@/views/CasesPage.vue"
6
import CaseDetailsView from "@/views/CaseDetailsView.vue"
7
import AgentsPage from "@/views/AgentsPage.vue"
8
+import AccessDenied from '@/components/common/AccessDenied.vue'
9
10
const NotFound = {
11
template: `
@@ -61,6 +62,12 @@ const routes = [
62
component: AgentsPage,
63
meta: { requiresAuth: true }
64
},
65
+ {
66
+ path: '/access-denied',
67
+ name: 'AccessDenied',
68
+ component: AccessDenied,
69
+ meta: { requiresAuth: false }
70
+ },
71
{
72
path: "/:pathMatch(.*)*",
73
name: "NotFound",
frontend/src/components/common/AccessDenied.vue
new
+61
@@ -0,0 +1,61 @@
1
+<!-- filepath: /Users/taylor/Desktop/Repos/CoPilot/frontend/src/components/common/AccessDenied.vue -->
2
+<template>
3
+ <div class="access-denied-container">
4
+ <n-result
5
+ status="403"
6
+ title="Access Denied"
7
+ :description="description || 'You do not have permission to view this page.'"
8
+ >
9
+ <template #footer>
10
+ <n-space justify="center">
11
+ <n-button type="primary" @click="goBack">
12
+ <template #icon>
13
+ <n-icon>
14
+ <ArrowBackOutline />
15
+ </n-icon>
16
+ </template>
17
+ Go Back
18
+ </n-button>
19
+ <n-button @click="goHome">
20
+ <template #icon>
21
+ <n-icon>
22
+ <HomeOutline />
23
+ </n-icon>
24
+ </template>
25
+ Go to Dashboard
26
+ </n-button>
27
+ </n-space>
28
+ </template>
29
+ </n-result>
30
+ </div>
31
+</template>
32
+
33
+<script setup lang="ts">
34
+import { ArrowBackOutline, HomeOutline } from '@vicons/ionicons5'
35
+import { useRouter } from 'vue-router'
36
+
37
+interface Props {
38
+ description?: string
39
+}
40
+
41
+defineProps<Props>()
42
+
43
+const router = useRouter()
44
+
45
+function goBack() {
46
+ router.back()
47
+}
48
+
49
+function goHome() {
50
+ router.push('/')
51
+}
52
+</script>
53
+
54
+<style scoped>
55
+.access-denied-container {
56
+ display: flex;
57
+ align-items: center;
58
+ justify-content: center;
59
+ min-height: 60vh;
60
+}
61
+</style>